Browse Source

Merge pull request #2851 from armink/add_guiconfig

[tools] add menuconfig GUI by python.
Bernard Xiong 6 years ago
parent
commit
7278bcb5f2
6 changed files with 2863 additions and 1427 deletions
  1. 5 5
      tools/building.py
  2. 43 0
      tools/defconfig.py
  3. 491 207
      tools/kconfiglib.py
  4. 11 9
      tools/menuconfig.py
  5. 2313 0
      tools/pyguiconfig.py
  6. 0 1206
      tools/pymenuconfig.py

+ 5 - 5
tools/building.py

@@ -377,7 +377,7 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
                 dest = 'pyconfig',
                 action = 'store_true',
                 default = False,
-                help = 'make menuconfig for RT-Thread BSP')
+                help = 'Python GUI menuconfig for RT-Thread BSP')
     AddOption('--pyconfig-silent',
                 dest = 'pyconfig_silent',
                 action = 'store_true',
@@ -385,14 +385,14 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
                 help = 'Don`t show pyconfig window')
 
     if GetOption('pyconfig_silent'):    
-        from menuconfig import pyconfig_silent
+        from menuconfig import guiconfig_silent
 
-        pyconfig_silent(Rtt_Root)
+        guiconfig_silent(Rtt_Root)
         exit(0)
     elif GetOption('pyconfig'):
-        from menuconfig import pyconfig
+        from menuconfig import guiconfig
 
-        pyconfig(Rtt_Root)
+        guiconfig(Rtt_Root)
         exit(0)
 
     configfn = GetOption('useconfig')

+ 43 - 0
tools/defconfig.py

@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2019, Ulf Magnusson
+# SPDX-License-Identifier: ISC
+
+"""
+Reads a specified configuration file, then writes a new configuration file.
+This can be used to initialize the configuration from e.g. an arch-specific
+configuration file. This input configuration file would usually be a minimal
+configuration file, as generated by e.g. savedefconfig.
+
+The default output filename is '.config'. A different filename can be passed in
+the KCONFIG_CONFIG environment variable.
+"""
+import argparse
+
+import kconfiglib
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        description=__doc__)
+
+    parser.add_argument(
+        "--kconfig",
+        default="Kconfig",
+        help="Base Kconfig file (default: Kconfig)")
+
+    parser.add_argument(
+        "config",
+        metavar="CONFIGURATION",
+        help="Input configuration file")
+
+    args = parser.parse_args()
+
+    kconf = kconfiglib.Kconfig(args.kconfig)
+    print(kconf.load_config(args.config))
+    print(kconf.write_config())
+
+
+if __name__ == "__main__":
+    main()

File diff suppressed because it is too large
+ 491 - 207
tools/kconfiglib.py


+ 11 - 9
tools/menuconfig.py

@@ -21,6 +21,7 @@
 # Date           Author       Notes
 # 2017-12-29     Bernard      The first version
 # 2018-07-31     weety        Support pyconfig
+# 2019-07-13     armink       Support guiconfig
 
 import os
 import re
@@ -225,9 +226,9 @@ def menuconfig(RTT_ROOT):
     if mtime != mtime2:
         mk_rtconfig(fn)
 
-# pyconfig for windows and linux
-def pyconfig(RTT_ROOT):
-    import pymenuconfig
+# guiconfig for windows and linux
+def guiconfig(RTT_ROOT):
+    import pyguiconfig
 
     touch_env()
     env_dir = get_env_dir()
@@ -241,7 +242,8 @@ def pyconfig(RTT_ROOT):
     else:
         mtime = -1
 
-    pymenuconfig.main(['--kconfig', 'Kconfig', '--config', '.config'])
+    sys.argv = ['guiconfig', 'Kconfig'];
+    pyguiconfig._main()
 
     if os.path.isfile(fn):
         mtime2 = os.path.getmtime(fn)
@@ -253,10 +255,9 @@ def pyconfig(RTT_ROOT):
         mk_rtconfig(fn)
 
 
-# pyconfig_silent for windows and linux
-def pyconfig_silent(RTT_ROOT):
-    import pymenuconfig
-    print("In pyconfig silent mode. Don`t display menuconfig window.")
+# guiconfig for windows and linux
+def guiconfig_silent(RTT_ROOT):
+    import defconfig
 
     touch_env()
     env_dir = get_env_dir()
@@ -265,7 +266,8 @@ def pyconfig_silent(RTT_ROOT):
 
     fn = '.config'
 
-    pymenuconfig.main(['--kconfig', 'Kconfig', '--config', '.config', '--silent', 'True'])
+    sys.argv = ['defconfig', '--kconfig', 'Kconfig', '.config']
+    defconfig.main()
 
     # silent mode, force to make rtconfig.h
     mk_rtconfig(fn)

+ 2313 - 0
tools/pyguiconfig.py

@@ -0,0 +1,2313 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2019, Ulf Magnusson
+# SPDX-License-Identifier: ISC
+
+"""
+Overview
+========
+
+A Tkinter-based menuconfig implementation, based around a treeview control and
+a help display. The interface should feel familiar to people used to qconf
+('make xconfig'). Compatible with both Python 2 and Python 3.
+
+The display can be toggled between showing the full tree and showing just a
+single menu (like menuconfig.py). Only single-menu mode distinguishes between
+symbols defined with 'config' and symbols defined with 'menuconfig'.
+
+A show-all mode is available that shows invisible items in red.
+
+Supports both mouse and keyboard controls. The following keyboard shortcuts are
+available:
+
+  Ctrl-S   : Save configuration
+  Ctrl-O   : Open configuration
+  Ctrl-A   : Toggle show-all mode
+  Ctrl-N   : Toggle show-name mode
+  Ctrl-M   : Toggle single-menu mode
+  Ctrl-F, /: Open jump-to dialog
+  ESC      : Close
+
+Running
+=======
+
+guiconfig.py can be run either as a standalone executable or by calling the
+menuconfig() function with an existing Kconfig instance. The second option is a
+bit inflexible in that it will still load and save .config, etc.
+
+When run in standalone mode, the top-level Kconfig file to load can be passed
+as a command-line argument. With no argument, it defaults to "Kconfig".
+
+The KCONFIG_CONFIG environment variable specifies the .config file to load (if
+it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used.
+
+When overwriting a configuration file, the old version is saved to
+<filename>.old (e.g. .config.old).
+
+$srctree is supported through Kconfiglib.
+"""
+
+# Note: There's some code duplication with menuconfig.py below, especially for
+# the help text. Maybe some of it could be moved into kconfiglib.py or a shared
+# helper script, but OTOH it's pretty nice to have things standalone and
+# customizable.
+
+import errno
+import os
+import sys
+
+_PY2 = sys.version_info[0] < 3
+
+if _PY2:
+    # Python 2
+    from Tkinter import *
+    import ttk
+    import tkFont as font
+    import tkFileDialog as filedialog
+    import tkMessageBox as messagebox
+else:
+    # Python 3
+    from tkinter import *
+    import tkinter.ttk as ttk
+    import tkinter.font as font
+    from tkinter import filedialog, messagebox
+
+from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \
+                       BOOL, TRISTATE, STRING, INT, HEX, \
+                       AND, OR, \
+                       expr_str, expr_value, split_expr, \
+                       standard_sc_expr_str, \
+                       TRI_TO_STR, TYPE_TO_STR, \
+                       standard_kconfig, standard_config_filename
+
+
+# If True, use GIF image data embedded in this file instead of separate GIF
+# files. See _load_images().
+_USE_EMBEDDED_IMAGES = True
+
+
+# Help text for the jump-to dialog
+_JUMP_TO_HELP = """\
+Type one or more strings/regexes and press Enter to list items that match all
+of them. Python's regex flavor is used (see the 're' module). Double-clicking
+an item will jump to it. Item values can be toggled directly within the dialog.\
+"""
+
+
+def _main():
+    menuconfig(standard_kconfig())
+
+
+# Global variables used below:
+#
+#   _root:
+#     The Toplevel instance for the main window
+#
+#   _tree:
+#     The Treeview in the main window
+#
+#   _jump_to_tree:
+#     The Treeview in the jump-to dialog. None if the jump-to dialog isn't
+#     open. Doubles as a flag.
+#
+#   _jump_to_matches:
+#     List of Nodes shown in the jump-to dialog
+#
+#   _menupath:
+#     The Label that shows the menu path of the selected item
+#
+#   _backbutton:
+#     The button shown in single-menu mode for jumping to the parent menu
+#
+#   _status_label:
+#     Label with status text shown at the bottom of the main window
+#     ("Modified", "Saved to ...", etc.)
+#
+#   _id_to_node:
+#     We can't use Node objects directly as Treeview item IDs, so we use their
+#     id()s instead. This dictionary maps Node id()s back to Nodes. (The keys
+#     are actually str(id(node)), just to simplify lookups.)
+#
+#   _cur_menu:
+#     The current menu. Ignored outside single-menu mode.
+#
+#   _show_all_var/_show_name_var/_single_menu_var:
+#     Tkinter Variable instances bound to the corresponding checkboxes
+#
+#   _show_all/_single_menu:
+#     Plain Python bools that track _show_all_var and _single_menu_var, to
+#     speed up and simplify things a bit
+#
+#   _conf_filename:
+#     File to save the configuration to
+#
+#   _minconf_filename:
+#     File to save minimal configurations to
+#
+#   _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.
+#
+#   _*_img:
+#     PhotoImage instances for images
+
+
+def menuconfig(kconf):
+    """
+    Launches the configuration interface, returning after the user exits.
+
+    kconf:
+      Kconfig instance to be configured
+    """
+    global _kconf
+    global _conf_filename
+    global _minconf_filename
+    global _jump_to_tree
+    global _cur_menu
+
+    _kconf = kconf
+
+    _jump_to_tree = None
+
+    _create_id_to_node()
+
+    _create_ui()
+
+    # Filename to save configuration to
+    _conf_filename = standard_config_filename()
+
+    # Load existing configuration and check if it's outdated
+    _set_conf_changed(_load_config())
+
+    # Filename to save minimal configuration to
+    _minconf_filename = "defconfig"
+
+    # Current menu in single-menu mode
+    _cur_menu = _kconf.top_node
+
+    # Any visible items in the top menu?
+    if not _shown_menu_nodes(kconf.top_node):
+        # Nothing visible. Start in show-all mode and try again.
+        _show_all_var.set(True)
+        if not _shown_menu_nodes(kconf.top_node):
+            # Give up and show an error. It's nice to be able to assume that
+            # the tree is non-empty in the rest of the code.
+            _root.wait_visibility()
+            messagebox.showerror(
+                "Error",
+                "Empty configuration -- nothing to configure.\n\n"
+                "Check that environment variables are set properly.")
+            _root.destroy()
+            return
+
+    # Build the initial tree
+    _update_tree()
+
+    # Select the first item and focus the Treeview, so that keyboard controls
+    # work immediately
+    _select(_tree, _tree.get_children()[0])
+    _tree.focus_set()
+
+    # Make geometry information available for centering the window. This
+    # indirectly creates the window, so hide it so that it's never shown at the
+    # old location.
+    _root.withdraw()
+    _root.update_idletasks()
+
+    # Center the window
+    _root.geometry("+{}+{}".format(
+        (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2,
+        (_root.winfo_screenheight() - _root.winfo_reqheight())//2))
+
+    # Show it
+    _root.deiconify()
+
+    # Prevent the window from being automatically resized. Otherwise, it
+    # changes size when scrollbars appear/disappear before the user has
+    # manually resized it.
+    _root.geometry(_root.geometry())
+
+    _root.mainloop()
+
+
+def _load_config():
+    # Loads any existing .config file. See the Kconfig.load_config() docstring.
+    #
+    # Returns True if .config is missing or outdated. We always prompt for
+    # saving the configuration in that case.
+
+    print(_kconf.load_config())
+    if not os.path.exists(_conf_filename):
+        # No .config
+        return True
+
+    return _needs_save()
+
+
+def _needs_save():
+    # Returns True if a just-loaded .config file is outdated (would get
+    # modified when saving)
+
+    if _kconf.missing_syms:
+        # Assignments to undefined symbols in the .config
+        return True
+
+    for sym in _kconf.unique_defined_syms:
+        if sym.user_value is None:
+            if sym.config_string:
+                # Unwritten symbol
+                return True
+        elif sym.orig_type in (BOOL, TRISTATE):
+            if sym.tri_value != sym.user_value:
+                # Written bool/tristate symbol, new value
+                return True
+        elif sym.str_value != sym.user_value:
+            # Written string/int/hex symbol, new value
+            return True
+
+    # No need to prompt for save
+    return False
+
+
+def _create_id_to_node():
+    global _id_to_node
+
+    _id_to_node = {str(id(node)): node for node in _kconf.node_iter()}
+
+
+def _create_ui():
+    # Creates the main window UI
+
+    global _root
+    global _tree
+
+    # Create the root window. This initializes Tkinter and makes e.g.
+    # PhotoImage available, so do it early.
+    _root = Tk()
+
+    _load_images()
+    _init_misc_ui()
+    _fix_treeview_issues()
+
+    _create_top_widgets()
+    # Create the pane with the Kconfig tree and description text
+    panedwindow, _tree = _create_kconfig_tree_and_desc(_root)
+    panedwindow.grid(column=0, row=1, sticky="nsew")
+    _create_status_bar()
+
+    _root.columnconfigure(0, weight=1)
+    # Only the pane with the Kconfig tree and description grows vertically
+    _root.rowconfigure(1, weight=1)
+
+    # Start with show-name disabled
+    _do_showname()
+
+    _tree.bind("<Left>", _tree_left_key)
+    _tree.bind("<Right>", _tree_right_key)
+    # Note: Binding this for the jump-to tree as well would cause issues due to
+    # the Tk bug mentioned in _tree_open()
+    _tree.bind("<<TreeviewOpen>>", _tree_open)
+    # add=True to avoid overriding the description text update
+    _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True)
+
+    _root.bind("<Control-s>", _save)
+    _root.bind("<Control-o>", _open)
+    _root.bind("<Control-a>", _toggle_showall)
+    _root.bind("<Control-n>", _toggle_showname)
+    _root.bind("<Control-m>", _toggle_tree_mode)
+    _root.bind("<Control-f>", _jump_to_dialog)
+    _root.bind("/", _jump_to_dialog)
+    _root.bind("<Escape>", _on_quit)
+
+
+def _load_images():
+    # Loads GIF images, creating the global _*_img PhotoImage variables.
+    # Base64-encoded images embedded in this script are used if
+    # _USE_EMBEDDED_IMAGES is True, and separate image files in the same
+    # directory as the script otherwise.
+    #
+    # Using a global variable indirectly prevents the image from being
+    # garbage-collected. Passing an image to a Tkinter function isn't enough to
+    # keep it alive.
+
+    def load_image(name, data):
+        var_name = "_{}_img".format(name)
+
+        if _USE_EMBEDDED_IMAGES:
+            globals()[var_name] = PhotoImage(data=data, format="gif")
+        else:
+            globals()[var_name] = PhotoImage(
+                file=os.path.join(os.path.dirname(__file__), name + ".gif"),
+                format="gif")
+
+    # Note: Base64 data can be put on the clipboard with
+    #   $ base64 -w0 foo.gif | xclip
+
+    load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=")
+    load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=")
+    load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=")
+    load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=")
+    load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=")
+    load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7")
+    load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=")
+    load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==")
+    load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==")
+    load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==")
+    load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==")
+    load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7")
+    load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=")
+    load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=")
+
+
+def _fix_treeview_issues():
+    # Fixes some Treeview issues
+
+    global _treeview_rowheight
+
+    style = ttk.Style()
+
+    # The treeview rowheight isn't adjusted automatically on high-DPI displays,
+    # so do it ourselves. The font will probably always be TkDefaultFont, but
+    # play it safe and look it up.
+
+    _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \
+        .metrics("linespace") + 2
+
+    style.configure("Treeview", rowheight=_treeview_rowheight)
+
+    # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae,
+    # which breaks tag background colors
+
+    for option in "foreground", "background":
+        # Filter out any styles starting with ("!disabled", "!selected", ...).
+        # style.map() returns an empty list for missing options, so this should
+        # be future-safe.
+        style.map(
+            "Treeview",
+            **{option: [elm for elm in style.map("Treeview", query_opt=option)
+                        if elm[:2] != ("!disabled", "!selected")]})
+
+
+def _init_misc_ui():
+    # Does misc. UI initialization, like setting the title, icon, and theme
+
+    _root.title(_kconf.mainmenu_text)
+    # iconphoto() isn't available in Python 2's Tkinter
+    _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img)
+    # Reducing the width of the window to 1 pixel makes it move around, at
+    # least on GNOME. Prevent weird stuff like that.
+    _root.minsize(128, 128)
+    _root.protocol("WM_DELETE_WINDOW", _on_quit)
+
+    # Use the 'clam' theme on *nix if it's available. It looks nicer than the
+    # 'default' theme.
+    if _root.tk.call("tk", "windowingsystem") == "x11":
+        style = ttk.Style()
+        if "clam" in style.theme_names():
+            style.theme_use("clam")
+
+
+def _create_top_widgets():
+    # Creates the controls above the Kconfig tree in the main window
+
+    global _show_all_var
+    global _show_name_var
+    global _single_menu_var
+    global _menupath
+    global _backbutton
+
+    topframe = ttk.Frame(_root)
+    topframe.grid(column=0, row=0, sticky="ew")
+
+    ttk.Button(topframe, text="Save", command=_save) \
+        .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c")
+
+    ttk.Button(topframe, text="Save as...", command=_save_as) \
+        .grid(column=1, row=0, sticky="ew")
+
+    ttk.Button(topframe, text="Save minimal (advanced)...",
+               command=_save_minimal) \
+        .grid(column=2, row=0, sticky="ew", padx=".05c")
+
+    ttk.Button(topframe, text="Open...", command=_open) \
+        .grid(column=3, row=0)
+
+    ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \
+        .grid(column=4, row=0, padx=".05c")
+
+    _show_name_var = BooleanVar()
+    ttk.Checkbutton(topframe, text="Show name", command=_do_showname,
+                    variable=_show_name_var) \
+        .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c",
+              ipady=".2c")
+
+    _show_all_var = BooleanVar()
+    ttk.Checkbutton(topframe, text="Show all", command=_do_showall,
+                    variable=_show_all_var) \
+        .grid(column=1, row=1, sticky="nsew", pady="0 .05c")
+
+    # Allow the show-all and single-menu status to be queried via plain global
+    # Python variables, which is faster and simpler
+
+    def show_all_updated(*_):
+        global _show_all
+        _show_all = _show_all_var.get()
+
+    _trace_write(_show_all_var, show_all_updated)
+    _show_all_var.set(False)
+
+    _single_menu_var = BooleanVar()
+    ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode,
+                    variable=_single_menu_var) \
+        .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c")
+
+    _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu,
+                             state="disabled")
+    _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c")
+
+    def tree_mode_updated(*_):
+        global _single_menu
+        _single_menu = _single_menu_var.get()
+
+        if _single_menu:
+            _backbutton.grid()
+        else:
+            _backbutton.grid_remove()
+
+    _trace_write(_single_menu_var, tree_mode_updated)
+    _single_menu_var.set(False)
+
+    # Column to the right of the buttons that the menu path extends into, so
+    # that it can grow wider than the buttons
+    topframe.columnconfigure(5, weight=1)
+
+    _menupath = ttk.Label(topframe)
+    _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c",
+                   pady="0 .05c")
+
+
+def _create_kconfig_tree_and_desc(parent):
+    # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text
+    # that shows a description of the selected node. Returns a tuple with the
+    # Panedwindow and the Treeview. This code is shared between the main window
+    # and the jump-to dialog.
+
+    panedwindow = ttk.Panedwindow(parent, orient=VERTICAL)
+
+    tree_frame, tree = _create_kconfig_tree(panedwindow)
+    desc_frame, desc = _create_kconfig_desc(panedwindow)
+
+    panedwindow.add(tree_frame, weight=1)
+    panedwindow.add(desc_frame)
+
+    def tree_select(_):
+        # The Text widget does not allow editing the text in its disabled
+        # state. We need to temporarily enable it.
+        desc["state"] = "normal"
+
+        sel = tree.selection()
+        if not sel:
+            desc.delete("1.0", "end")
+            desc["state"] = "disabled"
+            return
+
+        # Text.replace() is not available in Python 2's Tkinter
+        desc.delete("1.0", "end")
+        desc.insert("end", _info_str(_id_to_node[sel[0]]))
+
+        desc["state"] = "disabled"
+
+    tree.bind("<<TreeviewSelect>>", tree_select)
+    tree.bind("<1>", _tree_click)
+    tree.bind("<Double-1>", _tree_double_click)
+    tree.bind("<Return>", _tree_enter)
+    tree.bind("<KP_Enter>", _tree_enter)
+    tree.bind("<space>", _tree_toggle)
+    tree.bind("n", _tree_set_val(0))
+    tree.bind("m", _tree_set_val(1))
+    tree.bind("y", _tree_set_val(2))
+
+    return panedwindow, tree
+
+
+def _create_kconfig_tree(parent):
+    # Creates a Treeview for showing Kconfig nodes
+
+    frame = ttk.Frame(parent)
+
+    tree = ttk.Treeview(frame, selectmode="browse", height=20,
+                        columns=("name",))
+    tree.heading("#0", text="Option", anchor="w")
+    tree.heading("name", text="Name", anchor="w")
+
+    tree.tag_configure("n-bool", image=_n_bool_img)
+    tree.tag_configure("y-bool", image=_y_bool_img)
+    tree.tag_configure("m-tri", image=_m_tri_img)
+    tree.tag_configure("n-tri", image=_n_tri_img)
+    tree.tag_configure("m-tri", image=_m_tri_img)
+    tree.tag_configure("y-tri", image=_y_tri_img)
+    tree.tag_configure("m-my", image=_m_my_img)
+    tree.tag_configure("y-my", image=_y_my_img)
+    tree.tag_configure("n-locked", image=_n_locked_img)
+    tree.tag_configure("m-locked", image=_m_locked_img)
+    tree.tag_configure("y-locked", image=_y_locked_img)
+    tree.tag_configure("not-selected", image=_not_selected_img)
+    tree.tag_configure("selected", image=_selected_img)
+    tree.tag_configure("edit", image=_edit_img)
+    tree.tag_configure("invisible", foreground="red")
+
+    tree.grid(column=0, row=0, sticky="nsew")
+
+    _add_vscrollbar(frame, tree)
+
+    frame.columnconfigure(0, weight=1)
+    frame.rowconfigure(0, weight=1)
+
+    # Create items for all menu nodes. These can be detached/moved later.
+    # Micro-optimize this a bit.
+    insert = tree.insert
+    id_ = id
+    Symbol_ = Symbol
+    for node in _kconf.node_iter():
+        item = node.item
+        insert("", "end", iid=id_(node),
+               values=item.name if item.__class__ is Symbol_ else "")
+
+    return frame, tree
+
+
+def _create_kconfig_desc(parent):
+    # Creates a Text for showing the description of the selected Kconfig node
+
+    frame = ttk.Frame(parent)
+
+    desc = Text(frame, height=12, wrap="none", borderwidth=0,
+                state="disabled")
+    desc.grid(column=0, row=0, sticky="nsew")
+
+    # Work around not being to Ctrl-C/V text from a disabled Text widget, with a
+    # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only
+    desc.bind("<1>", lambda _: desc.focus_set())
+
+    _add_vscrollbar(frame, desc)
+
+    frame.columnconfigure(0, weight=1)
+    frame.rowconfigure(0, weight=1)
+
+    return frame, desc
+
+
+def _add_vscrollbar(parent, widget):
+    # Adds a vertical scrollbar to 'widget' that's only shown as needed
+
+    vscrollbar = ttk.Scrollbar(parent, orient="vertical",
+                               command=widget.yview)
+    vscrollbar.grid(column=1, row=0, sticky="ns")
+
+    def yscrollcommand(first, last):
+        # Only show the scrollbar when needed. 'first' and 'last' are
+        # strings.
+        if float(first) <= 0.0 and float(last) >= 1.0:
+            vscrollbar.grid_remove()
+        else:
+            vscrollbar.grid()
+
+        vscrollbar.set(first, last)
+
+    widget["yscrollcommand"] = yscrollcommand
+
+
+def _create_status_bar():
+    # Creates the status bar at the bottom of the main window
+
+    global _status_label
+
+    _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0")
+    _status_label.grid(column=0, row=3, sticky="ew")
+
+
+def _set_status(s):
+    # Sets the text in the status bar to 's'
+
+    _status_label["text"] = s
+
+
+def _set_conf_changed(changed):
+    # Updates the status re. whether there are unsaved changes
+
+    global _conf_changed
+
+    _conf_changed = changed
+    if changed:
+        _set_status("Modified")
+
+
+def _update_tree():
+    # Updates the Kconfig tree in the main window by first detaching all nodes
+    # and then updating and reattaching them. The tree structure might have
+    # changed.
+
+    # If a selected/focused item is detached and later reattached, it stays
+    # selected/focused. That can give multiple selections even though
+    # selectmode=browse. Save and later restore the selection and focus as a
+    # workaround.
+    old_selection = _tree.selection()
+    old_focus = _tree.focus()
+
+    # Detach all tree items before re-stringing them. This is relatively fast,
+    # luckily.
+    _tree.detach(*_id_to_node.keys())
+
+    if _single_menu:
+        _build_menu_tree()
+    else:
+        _build_full_tree(_kconf.top_node)
+
+    _tree.selection_set(old_selection)
+    _tree.focus(old_focus)
+
+
+def _build_full_tree(menu):
+    # Updates the tree starting from menu.list, in full-tree mode. To speed
+    # things up, only open menus are updated. The menu-at-a-time logic here is
+    # to deal with invisible items that can show up outside show-all mode (see
+    # _shown_full_nodes()).
+
+    for node in _shown_full_nodes(menu):
+        _add_to_tree(node, _kconf.top_node)
+
+        # _shown_full_nodes() includes nodes from menus rooted at symbols, so
+        # we only need to check "real" menus/choices here
+        if node.list and not isinstance(node.item, Symbol):
+            if _tree.item(id(node), "open"):
+                _build_full_tree(node)
+            else:
+                # We're just probing here, so _shown_menu_nodes() will work
+                # fine, and might be a bit faster
+                shown = _shown_menu_nodes(node)
+                if shown:
+                    # Dummy element to make the open/closed toggle appear
+                    _tree.move(id(shown[0]), id(shown[0].parent), "end")
+
+
+def _shown_full_nodes(menu):
+    # Returns the list of menu nodes shown in 'menu' (a menu node for a menu)
+    # for full-tree mode. A tricky detail is that invisible items need to be
+    # shown if they have visible children.
+
+    def rec(node):
+        res = []
+
+        while node:
+            if _visible(node) or _show_all:
+                res.append(node)
+                if node.list and isinstance(node.item, Symbol):
+                    # Nodes from menu created from dependencies
+                    res += rec(node.list)
+
+            elif node.list and isinstance(node.item, Symbol):
+                # Show invisible symbols (defined with either 'config' and
+                # 'menuconfig') if they have visible children. This can happen
+                # for an m/y-valued symbol with an optional prompt
+                # ('prompt "foo" is COND') that is currently disabled.
+                shown_children = rec(node.list)
+                if shown_children:
+                    res.append(node)
+                    res += shown_children
+
+            node = node.next
+
+        return res
+
+    return rec(menu.list)
+
+
+def _build_menu_tree():
+    # Updates the tree in single-menu mode. See _build_full_tree() as well.
+
+    for node in _shown_menu_nodes(_cur_menu):
+        _add_to_tree(node, _cur_menu)
+
+
+def _shown_menu_nodes(menu):
+    # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't
+    # include children of symbols defined with 'menuconfig'.
+
+    def rec(node):
+        res = []
+
+        while node:
+            if _visible(node) or _show_all:
+                res.append(node)
+                if node.list and not node.is_menuconfig:
+                    res += rec(node.list)
+
+            elif node.list and isinstance(node.item, Symbol):
+                shown_children = rec(node.list)
+                if shown_children:
+                    # Invisible item with visible children
+                    res.append(node)
+                    if not node.is_menuconfig:
+                        res += shown_children
+
+            node = node.next
+
+        return res
+
+    return rec(menu.list)
+
+
+def _visible(node):
+    # Returns True if the node should appear in the menu (outside show-all
+    # mode)
+
+    return node.prompt and expr_value(node.prompt[1]) and not \
+        (node.item == MENU and not expr_value(node.visibility))
+
+
+def _add_to_tree(node, top):
+    # Adds 'node' to the tree, at the end of its menu. We rely on going through
+    # the nodes linearly to get the correct order. 'top' holds the menu that
+    # corresponds to the top-level menu, and can vary in single-menu mode.
+
+    parent = node.parent
+    _tree.move(id(node), "" if parent is top else id(parent), "end")
+    _tree.item(
+        id(node),
+        text=_node_str(node),
+        # The _show_all test avoids showing invisible items in red outside
+        # show-all mode, which could look confusing/broken. Invisible symbols
+        # are shown outside show-all mode if an invisible symbol has visible
+        # children in an implicit menu.
+        tags=_img_tag(node) if _visible(node) or not _show_all else
+            _img_tag(node) + " invisible")
+
+
+def _node_str(node):
+    # Returns the string shown to the right of the image (if any) for the node
+
+    if node.prompt:
+        if node.item == COMMENT:
+            s = "*** {} ***".format(node.prompt[0])
+        else:
+            s = node.prompt[0]
+
+        if isinstance(node.item, Symbol):
+            sym = node.item
+
+            # Print "(NEW)" next to symbols without a user value (from e.g. a
+            # .config), but skip it for choice symbols in choices in y mode,
+            # and for symbols of UNKNOWN type (which generate a warning though)
+            if sym.user_value is None and sym.type and not \
+                (sym.choice and sym.choice.tri_value == 2):
+
+                s += " (NEW)"
+
+    elif isinstance(node.item, Symbol):
+        # Symbol without prompt (can show up in show-all)
+        s = "<{}>".format(node.item.name)
+
+    else:
+        # Choice without prompt. Use standard_sc_expr_str() so that it shows up
+        # as '<choice (name if any)>'.
+        s = standard_sc_expr_str(node.item)
+
+
+    if isinstance(node.item, Symbol):
+        sym = node.item
+        if sym.orig_type == STRING:
+            s += ": " + sym.str_value
+        elif sym.orig_type in (INT, HEX):
+            s = "({}) {}".format(sym.str_value, s)
+
+    elif isinstance(node.item, Choice) and node.item.tri_value == 2:
+        # Print the prompt of the selected symbol after the choice for
+        # choices in y mode
+        sym = node.item.selection
+        if sym:
+            for sym_node in sym.nodes:
+                # Use the prompt used at this choice location, in case the
+                # choice symbol is defined in multiple locations
+                if sym_node.parent is node and sym_node.prompt:
+                    s += " ({})".format(sym_node.prompt[0])
+                    break
+            else:
+                # If the symbol isn't defined at this choice location, then
+                # just use whatever prompt we can find for it
+                for sym_node in sym.nodes:
+                    if sym_node.prompt:
+                        s += " ({})".format(sym_node.prompt[0])
+                        break
+
+    # In single-menu mode, print "--->" next to nodes that have menus that can
+    # potentially be entered. Print "----" if the menu is empty. We don't allow
+    # those to be entered.
+    if _single_menu and node.is_menuconfig:
+        s += "  --->" if _shown_menu_nodes(node) else "  ----"
+
+    return s
+
+
+def _img_tag(node):
+    # Returns the tag for the image that should be shown next to 'node', or the
+    # empty string if it shouldn't have an image
+
+    item = node.item
+
+    if item in (MENU, COMMENT) or not item.orig_type:
+        return ""
+
+    if item.orig_type in (STRING, INT, HEX):
+        return "edit"
+
+    # BOOL or TRISTATE
+
+    if _is_y_mode_choice_sym(item):
+        # Choice symbol in y-mode choice
+        return "selected" if item.choice.selection is item else "not-selected"
+
+    if len(item.assignable) <= 1:
+        # Pinned to a single value
+        return "" if isinstance(item, Choice) else item.str_value + "-locked"
+
+    if item.type == BOOL:
+        return item.str_value + "-bool"
+
+    # item.type == TRISTATE
+    if item.assignable == (1, 2):
+        return item.str_value + "-my"
+    return item.str_value + "-tri"
+
+
+def _is_y_mode_choice_sym(item):
+    # The choice mode is an upper bound on the visibility of choice symbols, so
+    # we can check the choice symbols' own visibility to see if the choice is
+    # in y mode
+    return isinstance(item, Symbol) and item.choice and item.visibility == 2
+
+
+def _tree_click(event):
+    # Click on the Kconfig Treeview
+
+    tree = event.widget
+    if tree.identify_element(event.x, event.y) == "image":
+        item = tree.identify_row(event.y)
+        # Select the item before possibly popping up a dialog for
+        # string/int/hex items, so that its help is visible
+        _select(tree, item)
+        _change_node(_id_to_node[item], tree.winfo_toplevel())
+        return "break"
+
+
+def _tree_double_click(event):
+    # Double-click on the Kconfig treeview
+
+    # Do an extra check to avoid weirdness when double-clicking in the tree
+    # heading area
+    if not _in_heading(event):
+        return _tree_enter(event)
+
+
+def _in_heading(event):
+    # Returns True if 'event' took place in the tree heading
+
+    tree = event.widget
+    return hasattr(tree, "identify_region") and \
+        tree.identify_region(event.x, event.y) in ("heading", "separator")
+
+
+def _tree_enter(event):
+    # Enter press or double-click within the Kconfig treeview. Prefer to
+    # open/close/enter menus, but toggle the value if that's not possible.
+
+    tree = event.widget
+    sel = tree.focus()
+    if sel:
+        node = _id_to_node[sel]
+
+        if tree.get_children(sel):
+            _tree_toggle_open(sel)
+        elif _single_menu_mode_menu(node, tree):
+            _enter_menu_and_select_first(node)
+        else:
+            _change_node(node, tree.winfo_toplevel())
+
+        return "break"
+
+
+def _tree_toggle(event):
+    # Space press within the Kconfig treeview. Prefer to toggle the value, but
+    # open/close/enter the menu if that's not possible.
+
+    tree = event.widget
+    sel = tree.focus()
+    if sel:
+        node = _id_to_node[sel]
+
+        if _changeable(node):
+            _change_node(node, tree.winfo_toplevel())
+        elif _single_menu_mode_menu(node, tree):
+            _enter_menu_and_select_first(node)
+        elif tree.get_children(sel):
+            _tree_toggle_open(sel)
+
+        return "break"
+
+
+def _tree_left_key(_):
+    # Left arrow key press within the Kconfig treeview
+
+    if _single_menu:
+        # Leave the current menu in single-menu mode
+        _leave_menu()
+        return "break"
+
+    # Otherwise, default action
+
+
+def _tree_right_key(_):
+    # Right arrow key press within the Kconfig treeview
+
+    sel = _tree.focus()
+    if sel:
+        node = _id_to_node[sel]
+        # If the node can be entered in single-menu mode, do it
+        if _single_menu_mode_menu(node, _tree):
+            _enter_menu_and_select_first(node)
+            return "break"
+
+    # Otherwise, default action
+
+
+def _single_menu_mode_menu(node, tree):
+    # Returns True if single-menu mode is on and 'node' is an (interface)
+    # menu that can be entered
+
+    return _single_menu and tree is _tree and node.is_menuconfig and \
+           _shown_menu_nodes(node)
+
+
+def _changeable(node):
+    # Returns True if 'node' is a Symbol/Choice whose value can be changed
+
+    sc = node.item
+
+    if not isinstance(sc, (Symbol, Choice)):
+        return False
+
+    # This will hit for invisible symbols, which appear in show-all mode and
+    # when an invisible symbol has visible children (which can happen e.g. for
+    # symbols with optional prompts)
+    if not (node.prompt and expr_value(node.prompt[1])):
+        return False
+
+    return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \
+           or _is_y_mode_choice_sym(sc)
+
+
+def _tree_toggle_open(item):
+    # Opens/closes the Treeview item 'item'
+
+    if _tree.item(item, "open"):
+        _tree.item(item, open=False)
+    else:
+        node = _id_to_node[item]
+        if not isinstance(node.item, Symbol):
+            # Can only get here in full-tree mode
+            _build_full_tree(node)
+        _tree.item(item, open=True)
+
+
+def _tree_set_val(tri_val):
+    def tree_set_val(event):
+        # n/m/y press within the Kconfig treeview
+
+        # Sets the value of the currently selected item to 'tri_val', if that
+        # value can be assigned
+
+        sel = event.widget.focus()
+        if sel:
+            sc = _id_to_node[sel].item
+            if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable:
+                _set_val(sc, tri_val)
+
+    return tree_set_val
+
+
+def _tree_open(_):
+    # Lazily populates the Kconfig tree when menus are opened in full-tree mode
+
+    if _single_menu:
+        # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e
+        # ("ttk::treeview open/closed indicators can be toggled while hidden").
+        # Clicking on the hidden indicator will call _build_full_tree() in
+        # single-menu mode otherwise.
+        return
+
+    node = _id_to_node[_tree.focus()]
+    # _shown_full_nodes() includes nodes from menus rooted at symbols, so we
+    # only need to check "real" menus and choices here
+    if not isinstance(node.item, Symbol):
+        _build_full_tree(node)
+
+
+def _update_menu_path(_):
+    # Updates the displayed menu path when nodes are selected in the Kconfig
+    # treeview
+
+    sel = _tree.selection()
+    _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else ""
+
+
+def _item_row(item):
+    # Returns the row number 'item' appears on within the Kconfig treeview,
+    # starting from the top of the tree. Used to preserve scrolling.
+    #
+    # ttkTreeview.c in the Tk sources defines a RowNumber() function that does
+    # the same thing, but it's not exposed.
+
+    row = 0
+
+    while True:
+        prev = _tree.prev(item)
+        if prev:
+            item = prev
+            row += _n_rows(item)
+        else:
+            item = _tree.parent(item)
+            if not item:
+                return row
+            row += 1
+
+
+def _n_rows(item):
+    # _item_row() helper. Returns the number of rows occupied by 'item' and #
+    # its children.
+
+    rows = 1
+
+    if _tree.item(item, "open"):
+        for child in _tree.get_children(item):
+            rows += _n_rows(child)
+
+    return rows
+
+
+def _attached(item):
+    # Heuristic for checking if a Treeview item is attached. Doesn't seem to be
+    # good APIs for this. Might fail for super-obscure cases with tiny trees,
+    # but you'd just get a small scroll mess-up.
+
+    return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item))
+
+
+def _change_node(node, parent):
+    # Toggles/changes the value of 'node'. 'parent' is the parent window
+    # (either the main window or the jump-to dialog), in case we need to pop up
+    # a dialog.
+
+    if not _changeable(node):
+        return
+
+    # sc = symbol/choice
+    sc = node.item
+
+    if sc.type in (INT, HEX, STRING):
+        s = _set_val_dialog(node, parent)
+
+        # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib
+        # can't deal with. UTF-8-encode the string to work around it.
+        if _PY2 and isinstance(s, unicode):
+            s = s.encode("utf-8", "ignore")
+
+        if s is not None:
+            _set_val(sc, s)
+
+    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.
+        _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)
+        _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)])
+
+
+def _set_val(sc, val):
+    # Wrapper around Symbol/Choice.set_value() for updating the menu state and
+    # _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)
+        _set_conf_changed(True)
+
+        # Update the tree and try to preserve the scroll. Do a cheaper variant
+        # than in the show-all case, that might mess up the scroll slightly in
+        # rare cases, but is fast and flicker-free.
+
+        stayput = _loc_ref_item()  # Item to preserve scroll for
+        old_row = _item_row(stayput)
+
+        _update_tree()
+
+        # If the reference item disappeared (can happen if the change was done
+        # from the jump-to dialog), then avoid messing with the scroll and hope
+        # for the best
+        if _attached(stayput):
+            _tree.yview_scroll(_item_row(stayput) - old_row, "units")
+
+        if _jump_to_tree:
+            _update_jump_to_display()
+
+
+def _set_val_dialog(node, parent):
+    # Pops up a dialog for setting the value of the string/int/hex
+    # symbol at node 'node'. 'parent' is the parent window.
+
+    def ok(_=None):
+        # No 'nonlocal' in Python 2
+        global _entry_res
+
+        s = entry.get()
+        if sym.type == HEX and not s.startswith(("0x", "0X")):
+            s = "0x" + s
+
+        if _check_valid(dialog, entry, sym, s):
+            _entry_res = s
+            dialog.destroy()
+
+    def cancel(_=None):
+        global _entry_res
+        _entry_res = None
+        dialog.destroy()
+
+    sym = node.item
+
+    dialog = Toplevel(parent)
+    dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type]))
+    dialog.resizable(False, False)
+    dialog.transient(parent)
+    dialog.protocol("WM_DELETE_WINDOW", cancel)
+
+    ttk.Label(dialog, text=node.prompt[0] + ":") \
+        .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c",
+              pady=".2c .05c")
+
+    entry = ttk.Entry(dialog, width=30)
+    # Start with the previous value in the editbox, selected
+    entry.insert(0, sym.str_value)
+    entry.selection_range(0, "end")
+    entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c")
+    entry.focus_set()
+
+    range_info = _range_info(sym)
+    if range_info:
+        ttk.Label(dialog, text=range_info) \
+            .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c",
+                  pady=".2c 0")
+
+    ttk.Button(dialog, text="OK", command=ok) \
+        .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c",
+              pady=".4c")
+
+    ttk.Button(dialog, text="Cancel", command=cancel) \
+        .grid(column=1, row=4 if range_info else 3, padx="0 .3c")
+
+    # Give all horizontal space to the grid cell with the OK button, so that
+    # Cancel moves to the right
+    dialog.columnconfigure(0, weight=1)
+
+    _center_on_root(dialog)
+
+    # Hack to scroll the entry so that the end of the text is shown, from
+    # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail.
+    # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff
+    def scroll_entry(_):
+        _root.update_idletasks()
+        entry.unbind("<Expose>")
+        entry.xview_moveto(1)
+    entry.bind("<Expose>", scroll_entry)
+
+    # The dialog must be visible before we can grab the input
+    dialog.wait_visibility()
+    dialog.grab_set()
+
+    dialog.bind("<Return>", ok)
+    dialog.bind("<KP_Enter>", ok)
+    dialog.bind("<Escape>", cancel)
+
+    # Wait for the user to be done with the dialog
+    parent.wait_window(dialog)
+
+    # Regrab the input in the parent
+    parent.grab_set()
+
+    return _entry_res
+
+
+def _center_on_root(dialog):
+    # Centers 'dialog' on the root window. It often ends up at some bad place
+    # like the top-left corner of the screen otherwise. See the menuconfig()
+    # function, which has similar logic.
+
+    dialog.withdraw()
+    _root.update_idletasks()
+
+    dialog_width = dialog.winfo_reqwidth()
+    dialog_height = dialog.winfo_reqheight()
+
+    screen_width = _root.winfo_screenwidth()
+    screen_height = _root.winfo_screenheight()
+
+    x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2
+    y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2
+
+    # Clamp so that no part of the dialog is outside the screen
+    if x + dialog_width > screen_width:
+        x = screen_width - dialog_width
+    elif x < 0:
+        x = 0
+    if y + dialog_height > screen_height:
+        y = screen_height - dialog_height
+    elif y < 0:
+        y = 0
+
+    dialog.geometry("+{}+{}".format(x, y))
+
+    dialog.deiconify()
+
+
+def _check_valid(dialog, entry, sym, s):
+    # Returns True if the string 's' is a well-formed value for 'sym'.
+    # Otherwise, pops up an error and returns False.
+
+    if sym.type not in (INT, HEX):
+        # Anything goes for non-int/hex symbols
+        return True
+
+    base = 10 if sym.type == INT else 16
+    try:
+        int(s, base)
+    except ValueError:
+        messagebox.showerror(
+            "Bad value",
+            "'{}' is a malformed {} value".format(
+                s, TYPE_TO_STR[sym.type]),
+            parent=dialog)
+        entry.focus_set()
+        return False
+
+    for low_sym, high_sym, cond in sym.ranges:
+        if expr_value(cond):
+            low_s = low_sym.str_value
+            high_s = high_sym.str_value
+
+            if not int(low_s, base) <= int(s, base) <= int(high_s, base):
+                messagebox.showerror(
+                    "Value out of range",
+                    "{} is outside the range {}-{}".format(s, low_s, high_s),
+                    parent=dialog)
+                entry.focus_set()
+                return False
+
+            break
+
+    return True
+
+
+def _range_info(sym):
+    # Returns a string with information about the valid range for the symbol
+    # 'sym', or None if 'sym' doesn't have a range
+
+    if sym.type in (INT, HEX):
+        for low, high, cond in sym.ranges:
+            if expr_value(cond):
+                return "Range: {}-{}".format(low.str_value, high.str_value)
+
+    return None
+
+
+def _save(_=None):
+    # Tries to save the configuration
+
+    if _try_save(_kconf.write_config, _conf_filename, "configuration"):
+        _set_conf_changed(False)
+
+    _tree.focus_set()
+
+
+def _save_as():
+    # Pops up a dialog for saving the configuration to a specific location
+
+    global _conf_filename
+
+    filename = _conf_filename
+    while True:
+        filename = filedialog.asksaveasfilename(
+            title="Save configuration as",
+            initialdir=os.path.dirname(filename),
+            initialfile=os.path.basename(filename),
+            parent=_root)
+
+        if not filename:
+            break
+
+        if _try_save(_kconf.write_config, filename, "configuration"):
+            _conf_filename = filename
+            break
+
+    _tree.focus_set()
+
+
+def _save_minimal():
+    # Pops up a dialog for saving a minimal configuration (defconfig) to a
+    # specific location
+
+    global _minconf_filename
+
+    filename = _minconf_filename
+    while True:
+        filename = filedialog.asksaveasfilename(
+            title="Save minimal configuration as",
+            initialdir=os.path.dirname(filename),
+            initialfile=os.path.basename(filename),
+            parent=_root)
+
+        if not filename:
+            break
+
+        if _try_save(_kconf.write_min_config, filename,
+                     "minimal configuration"):
+
+            _minconf_filename = filename
+            break
+
+    _tree.focus_set()
+
+
+def _open(_=None):
+    # Pops up a dialog for loading a configuration
+
+    global _conf_filename
+
+    if _conf_changed and \
+        not messagebox.askokcancel(
+            "Unsaved changes",
+            "You have unsaved changes. Load new configuration anyway?"):
+
+        return
+
+    filename = _conf_filename
+    while True:
+        filename = filedialog.askopenfilename(
+            title="Open configuration",
+            initialdir=os.path.dirname(filename),
+            initialfile=os.path.basename(filename),
+            parent=_root)
+
+        if not filename:
+            break
+
+        if _try_load(filename):
+            # Maybe something fancier could be done here later to try to
+            # preserve the scroll
+
+            _conf_filename = filename
+            _set_conf_changed(_needs_save())
+
+            if _single_menu and not _shown_menu_nodes(_cur_menu):
+                # Turn on show-all if we're in single-menu mode and would end
+                # up with an empty menu
+                _show_all_var.set(True)
+
+            _update_tree()
+
+            break
+
+    _tree.focus_set()
+
+
+def _toggle_showname(_):
+    # Toggles show-name mode on/off
+
+    _show_name_var.set(not _show_name_var.get())
+    _do_showname()
+
+
+def _do_showname():
+    # Updates the UI for the current show-name setting
+
+    # Columns do not automatically shrink/expand, so we have to update
+    # column widths ourselves
+
+    tree_width = _tree.winfo_width()
+
+    if _show_name_var.get():
+        _tree["displaycolumns"] = ("name",)
+        _tree["show"] = "tree headings"
+        name_width = tree_width//3
+        _tree.column("#0", width=max(tree_width - name_width, 1))
+        _tree.column("name", width=name_width)
+    else:
+        _tree["displaycolumns"] = ()
+        _tree["show"] = "tree"
+        _tree.column("#0", width=tree_width)
+
+    _tree.focus_set()
+
+
+def _toggle_showall(_):
+    # Toggles show-all mode on/off
+
+    _show_all_var.set(not _show_all)
+    _do_showall()
+
+
+def _do_showall():
+    # Updates the UI for the current show-all setting
+
+    # Don't allow turning off show-all if we're in single-menu mode and the
+    # current menu would become empty
+    if _single_menu and not _shown_menu_nodes(_cur_menu):
+        _show_all_var.set(True)
+        return
+
+    # Save scroll information. old_scroll can end up negative here, if the
+    # reference item isn't shown (only invisible items on the screen, and
+    # show-all being turned off).
+
+    stayput = _vis_loc_ref_item()
+    # Probe the middle of the first row, to play it safe. identify_row(0) seems
+    # to return the row before the top row.
+    old_scroll = _item_row(stayput) - \
+        _item_row(_tree.identify_row(_treeview_rowheight//2))
+
+    _update_tree()
+
+    if _show_all:
+        # Deep magic: Unless we call update_idletasks(), the scroll adjustment
+        # below is restricted to the height of the old tree, instead of the
+        # height of the new tree. Since the tree with show-all on is guaranteed
+        # to be taller, and we want the maximum range, we only call it when
+        # turning show-all on.
+        #
+        # Strictly speaking, something similar ought to be done when changing
+        # symbol values, but it causes annoying flicker, and in 99% of cases
+        # things work anyway there (with usually minor scroll mess-ups in the
+        # 1% case).
+        _root.update_idletasks()
+
+    # Restore scroll
+    _tree.yview(_item_row(stayput) - old_scroll)
+
+    _tree.focus_set()
+
+
+def _toggle_tree_mode(_):
+    # Toggles single-menu mode on/off
+
+    _single_menu_var.set(not _single_menu)
+    _do_tree_mode()
+
+
+def _do_tree_mode():
+    # Updates the UI for the current tree mode (full-tree or single-menu)
+
+    loc_ref_node = _id_to_node[_loc_ref_item()]
+
+    if not _single_menu:
+        # _jump_to() -> _enter_menu() already updates the tree, but
+        # _jump_to() -> load_parents() doesn't, because it isn't always needed.
+        # We always need to update the tree here, e.g. to add/remove "--->".
+        _update_tree()
+
+    _jump_to(loc_ref_node)
+    _tree.focus_set()
+
+
+def _enter_menu_and_select_first(menu):
+    # Enters the menu 'menu' and selects the first item. Used in single-menu
+    # mode.
+
+    _enter_menu(menu)
+    _select(_tree, _tree.get_children()[0])
+
+
+def _enter_menu(menu):
+    # Enters the menu 'menu'. Used in single-menu mode.
+
+    global _cur_menu
+
+    _cur_menu = menu
+    _update_tree()
+
+    _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal"
+
+
+def _leave_menu():
+    # Leaves the current menu. Used in single-menu mode.
+
+    global _cur_menu
+
+    if _cur_menu is not _kconf.top_node:
+        old_menu = _cur_menu
+
+        _cur_menu = _parent_menu(_cur_menu)
+        _update_tree()
+
+        _select(_tree, id(old_menu))
+
+        if _cur_menu is _kconf.top_node:
+            _backbutton["state"] = "disabled"
+
+    _tree.focus_set()
+
+
+def _select(tree, item):
+    # Selects, focuses, and see()s 'item' in 'tree'
+
+    tree.selection_set(item)
+    tree.focus(item)
+    tree.see(item)
+
+
+def _loc_ref_item():
+    # Returns a Treeview item that can serve as a reference for the current
+    # scroll location. We try to make this item stay on the same row on the
+    # screen when updating the tree.
+
+    # If the selected item is visible, use that
+    sel = _tree.selection()
+    if sel and _tree.bbox(sel[0]):
+        return sel[0]
+
+    # Otherwise, use the middle item on the screen. If it doesn't exist, the
+    # tree is probably really small, so use the first item in the entire tree.
+    return _tree.identify_row(_tree.winfo_height()//2) or \
+        _tree.get_children()[0]
+
+
+def _vis_loc_ref_item():
+    # Like _loc_ref_item(), but finds a visible item around the reference item.
+    # Used when changing show-all mode, where non-visible (red) items will
+    # disappear.
+
+    item = _loc_ref_item()
+
+    vis_before = _vis_before(item)
+    if vis_before and _tree.bbox(vis_before):
+        return vis_before
+
+    vis_after = _vis_after(item)
+    if vis_after and _tree.bbox(vis_after):
+        return vis_after
+
+    return vis_before or vis_after
+
+
+def _vis_before(item):
+    # _vis_loc_ref_item() helper. Returns the first visible (not red) item,
+    # searching backwards from 'item'.
+
+    while item:
+        if not _tree.tag_has("invisible", item):
+            return item
+
+        prev = _tree.prev(item)
+        item = prev if prev else _tree.parent(item)
+
+    return None
+
+
+def _vis_after(item):
+    # _vis_loc_ref_item() helper. Returns the first visible (not red) item,
+    # searching forwards from 'item'.
+
+    while item:
+        if not _tree.tag_has("invisible", item):
+            return item
+
+        next = _tree.next(item)
+        if next:
+            item = next
+        else:
+            item = _tree.parent(item)
+            if not item:
+                break
+            item = _tree.next(item)
+
+    return None
+
+
+def _on_quit(_=None):
+    # Called when the user wants to exit
+
+    if not _conf_changed:
+        _quit("No changes to save (for '{}')".format(_conf_filename))
+        return
+
+    while True:
+        ync = messagebox.askyesnocancel("Quit", "Save changes?")
+        if ync is None:
+            return
+
+        if not ync:
+            _quit("Configuration ({}) was not saved".format(_conf_filename))
+            return
+
+        if _try_save(_kconf.write_config, _conf_filename, "configuration"):
+            # _try_save() already prints the "Configuration saved to ..."
+            # message
+            _quit()
+            return
+
+
+def _quit(msg=None):
+    # Quits the application
+
+    # Do not call sys.exit() here, in case we're being run from a script
+    _root.destroy()
+    if msg:
+        print(msg)
+
+
+def _try_save(save_fn, filename, description):
+    # Tries to save a configuration file. Pops up an error and returns False on
+    # failure.
+    #
+    # save_fn:
+    #   Function to call with 'filename' to save the file
+    #
+    # description:
+    #   String describing the thing being saved
+
+    try:
+        # save_fn() returns a message to print
+        msg = save_fn(filename)
+        _set_status(msg)
+        print(msg)
+        return True
+    except EnvironmentError as e:
+        messagebox.showerror(
+            "Error saving " + description,
+            "Error saving {} to '{}': {} (errno: {})"
+            .format(description, e.filename, e.strerror,
+                    errno.errorcode[e.errno]))
+        return False
+
+
+def _try_load(filename):
+    # Tries to load a configuration file. Pops up an error and returns False on
+    # failure.
+    #
+    # filename:
+    #   Configuration file to load
+
+    try:
+        msg = _kconf.load_config(filename)
+        _set_status(msg)
+        print(msg)
+        return True
+    except EnvironmentError as e:
+        messagebox.showerror(
+            "Error loading configuration",
+            "Error loading '{}': {} (errno: {})"
+            .format(filename, e.strerror, errno.errorcode[e.errno]))
+        return False
+
+
+def _jump_to_dialog(_=None):
+    # Pops up a dialog for jumping directly to a particular node. Symbol values
+    # can also be changed within the dialog.
+    #
+    # Note: There's nothing preventing this from doing an incremental search
+    # like menuconfig.py does, but currently it's a bit jerky for large Kconfig
+    # trees, at least when inputting the beginning of the search string. We'd
+    # need to somehow only update the tree items that are shown in the Treeview
+    # to fix it.
+
+    global _jump_to_tree
+
+    def search(_=None):
+        _update_jump_to_matches(msglabel, entry.get())
+
+    def jump_to_selected(event=None):
+        # Jumps to the selected node and closes the dialog
+
+        # Ignore double clicks on the image and in the heading area
+        if event and (tree.identify_element(event.x, event.y) == "image" or
+                      _in_heading(event)):
+            return
+
+        sel = tree.selection()
+        if not sel:
+            return
+
+        node = _id_to_node[sel[0]]
+
+        if node not in _shown_menu_nodes(_parent_menu(node)):
+            _show_all_var.set(True)
+            if not _single_menu:
+                # See comment in _do_tree_mode()
+                _update_tree()
+
+        _jump_to(node)
+
+        dialog.destroy()
+
+    def tree_select(_):
+        jumpto_button["state"] = "normal" if tree.selection() else "disabled"
+
+
+    dialog = Toplevel(_root)
+    dialog.geometry("+{}+{}".format(
+        _root.winfo_rootx() + 50, _root.winfo_rooty() + 50))
+    dialog.title("Jump to symbol/choice/menu/comment")
+    dialog.minsize(128, 128)  # See _create_ui()
+    dialog.transient(_root)
+
+    ttk.Label(dialog, text=_JUMP_TO_HELP) \
+        .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c",
+              pady=".1c")
+
+    entry = ttk.Entry(dialog)
+    entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c")
+    entry.focus_set()
+
+    entry.bind("<Return>", search)
+    entry.bind("<KP_Enter>", search)
+
+    ttk.Button(dialog, text="Search", command=search) \
+        .grid(column=1, row=1, padx="0 .1c", pady="0 .1c")
+
+    msglabel = ttk.Label(dialog)
+    msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c")
+
+    panedwindow, tree = _create_kconfig_tree_and_desc(dialog)
+    panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew")
+
+    # Clear tree
+    tree.set_children("")
+
+    _jump_to_tree = tree
+
+    jumpto_button = ttk.Button(dialog, text="Jump to selected item",
+                               state="disabled", command=jump_to_selected)
+    jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c")
+
+    dialog.columnconfigure(0, weight=1)
+    # Only the pane with the Kconfig tree and description grows vertically
+    dialog.rowconfigure(3, weight=1)
+
+    # See the menuconfig() function
+    _root.update_idletasks()
+    dialog.geometry(dialog.geometry())
+
+    # The dialog must be visible before we can grab the input
+    dialog.wait_visibility()
+    dialog.grab_set()
+
+    tree.bind("<Double-1>", jump_to_selected)
+    tree.bind("<Return>", jump_to_selected)
+    tree.bind("<KP_Enter>", jump_to_selected)
+    # add=True to avoid overriding the description text update
+    tree.bind("<<TreeviewSelect>>", tree_select, add=True)
+
+    dialog.bind("<Escape>", lambda _: dialog.destroy())
+
+    # Wait for the user to be done with the dialog
+    _root.wait_window(dialog)
+
+    _jump_to_tree = None
+
+    _tree.focus_set()
+
+
+def _update_jump_to_matches(msglabel, search_string):
+    # Searches for nodes matching the search string and updates
+    # _jump_to_matches. Puts a message in 'msglabel' if there are no matches,
+    # or regex errors.
+
+    global _jump_to_matches
+
+    _jump_to_tree.selection_set(())
+
+    try:
+        # We could use re.IGNORECASE here instead of lower(), but this is
+        # faster for regexes like '.*debug$' (though the '.*' is redundant
+        # there). Those probably have bad interactions with re.search(), which
+        # matches anywhere in the string.
+        regex_searches = [re.compile(regex).search
+                          for regex in search_string.lower().split()]
+    except re.error as e:
+        msg = "Bad regular expression"
+        # re.error.msg was added in Python 3.5
+        if hasattr(e, "msg"):
+            msg += ": " + e.msg
+        msglabel["text"] = msg
+        # Clear tree
+        _jump_to_tree.set_children("")
+        return
+
+    _jump_to_matches = []
+    add_match = _jump_to_matches.append
+
+    for node in _sorted_sc_nodes():
+        # Symbol/choice
+        sc = node.item
+
+        for search in regex_searches:
+            # Both the name and the prompt might be missing, since
+            # we're searching both symbols and choices
+
+            # Does the regex match either the symbol name or the
+            # prompt (if any)?
+            if not (sc.name and search(sc.name.lower()) or
+                    node.prompt and search(node.prompt[0].lower())):
+
+                # Give up on the first regex that doesn't match, to
+                # speed things up a bit when multiple regexes are
+                # entered
+                break
+
+        else:
+            add_match(node)
+
+    # Search menus and comments
+
+    for node in _sorted_menu_comment_nodes():
+        for search in regex_searches:
+            if not search(node.prompt[0].lower()):
+                break
+        else:
+            add_match(node)
+
+    msglabel["text"] = "" if _jump_to_matches else "No matches"
+
+    _update_jump_to_display()
+
+    if _jump_to_matches:
+        item = id(_jump_to_matches[0])
+        _jump_to_tree.selection_set(item)
+        _jump_to_tree.focus(item)
+
+
+def _update_jump_to_display():
+    # Updates the images and text for the items in _jump_to_matches, and sets
+    # them as the items of _jump_to_tree
+
+    # Micro-optimize a bit
+    item = _jump_to_tree.item
+    id_ = id
+    node_str = _node_str
+    img_tag = _img_tag
+    visible = _visible
+    for node in _jump_to_matches:
+        item(id_(node),
+             text=node_str(node),
+             tags=img_tag(node) if visible(node) else
+                 img_tag(node) + " invisible")
+
+    _jump_to_tree.set_children("", *map(id, _jump_to_matches))
+
+
+def _jump_to(node):
+    # Jumps directly to 'node' and selects it
+
+    if _single_menu:
+        _enter_menu(_parent_menu(node))
+    else:
+        _load_parents(node)
+
+    _select(_tree, id(node))
+
+
+# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing
+# to the same list. This avoids a global.
+def _sorted_sc_nodes(cached_nodes=[]):
+    # Returns a sorted list of symbol and choice nodes to search. The symbol
+    # nodes appear first, sorted by name, and then the choice nodes, sorted by
+    # prompt and (secondarily) name.
+
+    if not cached_nodes:
+        # Add symbol nodes
+        for sym in sorted(_kconf.unique_defined_syms,
+                          key=lambda sym: sym.name):
+            # += is in-place for lists
+            cached_nodes += sym.nodes
+
+        # Add choice nodes
+
+        choices = sorted(_kconf.unique_choices,
+                         key=lambda choice: choice.name or "")
+
+        cached_nodes += sorted(
+            [node
+             for choice in choices
+                 for node in choice.nodes],
+            key=lambda node: node.prompt[0] if node.prompt else "")
+
+    return cached_nodes
+
+
+def _sorted_menu_comment_nodes(cached_nodes=[]):
+    # Returns a list of menu and comment nodes to search, sorted by prompt,
+    # with the menus first
+
+    if not cached_nodes:
+        def prompt_text(mc):
+            return mc.prompt[0]
+
+        cached_nodes += sorted(_kconf.menus, key=prompt_text)
+        cached_nodes += sorted(_kconf.comments, key=prompt_text)
+
+    return cached_nodes
+
+
+def _load_parents(node):
+    # Menus are lazily populated as they're opened in full-tree mode, but
+    # jumping to an item needs its parent menus to be populated. This function
+    # populates 'node's parents.
+
+    # Get all parents leading up to 'node', sorted with the root first
+    parents = []
+    cur = node.parent
+    while cur is not _kconf.top_node:
+        parents.append(cur)
+        cur = cur.parent
+    parents.reverse()
+
+    for i, parent in enumerate(parents):
+        if not _tree.item(id(parent), "open"):
+            # Found a closed menu. Populate it and all the remaining menus
+            # leading up to 'node'.
+            for parent in parents[i:]:
+                # We only need to populate "real" menus/choices. Implicit menus
+                # are populated when their parents menus are entered.
+                if not isinstance(parent.item, Symbol):
+                    _build_full_tree(parent)
+            return
+
+
+def _parent_menu(node):
+    # Returns the menu node of the menu that contains 'node'. In addition to
+    # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'.
+    # "Menu" here means a menu in the interface.
+
+    menu = node.parent
+    while not menu.is_menuconfig:
+        menu = menu.parent
+    return menu
+
+
+def _trace_write(var, fn):
+    # Makes fn() be called whenever the Tkinter Variable 'var' changes value
+
+    # trace_variable() is deprecated according to the docstring,
+    # which recommends trace_add()
+    if hasattr(var, "trace_add"):
+        var.trace_add("write", fn)
+    else:
+        var.trace_variable("w", fn)
+
+
+def _info_str(node):
+    # Returns information about the menu node 'node' as a string.
+    #
+    # The helper functions are responsible for adding newlines. This allows
+    # them to return "" if they don't want to add any output.
+
+    if isinstance(node.item, Symbol):
+        sym = node.item
+
+        return (
+            _name_info(sym) +
+            _help_info(sym) +
+            _direct_dep_info(sym) +
+            _defaults_info(sym) +
+            _select_imply_info(sym) +
+            _kconfig_def_info(sym)
+        )
+
+    if isinstance(node.item, Choice):
+        choice = node.item
+
+        return (
+            _name_info(choice) +
+            _help_info(choice) +
+            'Mode: {}\n\n'.format(choice.str_value) +
+            _choice_syms_info(choice) +
+            _direct_dep_info(choice) +
+            _defaults_info(choice) +
+            _kconfig_def_info(choice)
+        )
+
+    # node.item in (MENU, COMMENT)
+    return _kconfig_def_info(node)
+
+
+def _name_info(sc):
+    # Returns a string with the name of the symbol/choice. Choices are shown as
+    # <choice (name if any)>.
+
+    return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n"
+
+
+def _value_info(sym):
+    # Returns a string showing 'sym's value
+
+    # Only put quotes around the value for string symbols
+    return "Value: {}\n".format(
+        '"{}"'.format(sym.str_value)
+        if sym.orig_type == STRING
+        else sym.str_value)
+
+
+def _choice_syms_info(choice):
+    # Returns a string listing the choice symbols in 'choice'. Adds
+    # "(selected)" next to the selected one.
+
+    s = "Choice symbols:\n"
+
+    for sym in choice.syms:
+        s += "  - " + sym.name
+        if sym is choice.selection:
+            s += " (selected)"
+        s += "\n"
+
+    return s + "\n"
+
+
+def _help_info(sc):
+    # Returns a string with the help text(s) of 'sc' (Symbol or Choice).
+    # Symbols and choices defined in multiple locations can have multiple help
+    # texts.
+
+    s = ""
+
+    for node in sc.nodes:
+        if node.help is not None:
+            s += node.help + "\n\n"
+
+    return s
+
+
+def _direct_dep_info(sc):
+    # Returns a string describing the direct dependencies of 'sc' (Symbol or
+    # Choice). The direct dependencies are the OR of the dependencies from each
+    # definition location. The dependencies at each definition location come
+    # from 'depends on' and dependencies inherited from parent items.
+
+    return "" if sc.direct_dep is _kconf.y else \
+        'Direct dependencies (={}):\n{}\n' \
+        .format(TRI_TO_STR[expr_value(sc.direct_dep)],
+                _split_expr_info(sc.direct_dep, 2))
+
+
+def _defaults_info(sc):
+    # Returns a string describing the defaults of 'sc' (Symbol or Choice)
+
+    if not sc.defaults:
+        return ""
+
+    s = "Defaults:\n"
+
+    for val, cond in sc.orig_defaults:
+        s += "  - "
+        if isinstance(sc, Symbol):
+            s += _expr_str(val)
+
+            # Skip the tristate value hint if the expression is just a single
+            # symbol. _expr_str() already shows its value as a string.
+            #
+            # This also avoids showing the tristate value for string/int/hex
+            # defaults, which wouldn't make any sense.
+            if isinstance(val, tuple):
+                s += '  (={})'.format(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 += val.name
+        s += "\n"
+
+        if cond is not _kconf.y:
+            s += "    Condition (={}):\n{}" \
+                 .format(TRI_TO_STR[expr_value(cond)],
+                         _split_expr_info(cond, 4))
+
+    return s + "\n"
+
+
+def _split_expr_info(expr, indent):
+    # Returns a string with 'expr' split into its top-level && or || operands,
+    # with one operand per line, together with the operand's value. This is
+    # usually enough to get something readable for long expressions. A fancier
+    # recursive thingy would be possible too.
+    #
+    # indent:
+    #   Number of leading spaces to add before the split expression.
+
+    if len(split_expr(expr, AND)) > 1:
+        split_op = AND
+        op_str = "&&"
+    else:
+        split_op = OR
+        op_str = "||"
+
+    s = ""
+    for i, term in enumerate(split_expr(expr, split_op)):
+        s += "{}{} {}".format(indent*" ",
+                              "  " if i == 0 else op_str,
+                              _expr_str(term))
+
+        # Don't bother showing the value hint if the expression is just a
+        # single symbol. _expr_str() already shows its value.
+        if isinstance(term, tuple):
+            s += "  (={})".format(TRI_TO_STR[expr_value(term)])
+
+        s += "\n"
+
+    return s
+
+
+def _select_imply_info(sym):
+    # Returns a string with information about which symbols 'select' or 'imply'
+    # 'sym'. The selecting/implying symbols are grouped according to which
+    # value they select/imply 'sym' to (n/m/y).
+
+    def sis(expr, val, title):
+        # sis = selects/implies
+        sis = [si for si in split_expr(expr, OR) if expr_value(si) == val]
+        if not sis:
+            return ""
+
+        res = title
+        for si in sis:
+            res += "  - {}\n".format(split_expr(si, AND)[0].name)
+        return res + "\n"
+
+    s = ""
+
+    if sym.rev_dep is not _kconf.n:
+        s += sis(sym.rev_dep, 2,
+                 "Symbols currently y-selecting this symbol:\n")
+        s += sis(sym.rev_dep, 1,
+                 "Symbols currently m-selecting this symbol:\n")
+        s += sis(sym.rev_dep, 0,
+                 "Symbols currently n-selecting this symbol (no effect):\n")
+
+    if sym.weak_rev_dep is not _kconf.n:
+        s += sis(sym.weak_rev_dep, 2,
+                 "Symbols currently y-implying this symbol:\n")
+        s += sis(sym.weak_rev_dep, 1,
+                 "Symbols currently m-implying this symbol:\n")
+        s += sis(sym.weak_rev_dep, 0,
+                 "Symbols currently n-implying this symbol (no effect):\n")
+
+    return s
+
+
+def _kconfig_def_info(item):
+    # Returns a string with the definition of 'item' in Kconfig syntax,
+    # together with the definition location(s) and their include and menu paths
+
+    nodes = [item] if isinstance(item, MenuNode) else item.nodes
+
+    s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \
+        .format("s" if len(nodes) > 1 else "")
+    s += (len(s) - 1)*"="
+
+    for node in nodes:
+        s += "\n\n" \
+             "At {}:{}\n" \
+             "{}" \
+             "Menu path: {}\n\n" \
+             "{}" \
+             .format(node.filename, node.linenr,
+                     _include_path_info(node),
+                     _menu_path_info(node),
+                     node.custom_str(_name_and_val_str))
+
+    return s
+
+
+def _include_path_info(node):
+    if not node.include_path:
+        # In the top-level Kconfig file
+        return ""
+
+    return "Included via {}\n".format(
+        " -> ".join("{}:{}".format(filename, linenr)
+                    for filename, linenr in node.include_path))
+
+
+def _menu_path_info(node):
+    # Returns a string describing the menu path leading up to 'node'
+
+    path = ""
+
+    while node.parent is not _kconf.top_node:
+        node = node.parent
+
+        # Promptless choices might appear among the parents. Use
+        # standard_sc_expr_str() for them, so that they show up as
+        # '<choice (name if any)>'.
+        path = " -> " + (node.prompt[0] if node.prompt else
+                         standard_sc_expr_str(node.item)) + path
+
+    return "(Top)" + path
+
+
+def _name_and_val_str(sc):
+    # Custom symbol/choice printer that shows symbol values after symbols
+
+    # Show the values of non-constant (non-quoted) symbols that don't look like
+    # numbers. Things like 123 are actually symbol references, and only work as
+    # expected due to undefined symbols getting their name as their value.
+    # Showing the symbol value for those isn't helpful though.
+    if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name):
+        if not sc.nodes:
+            # Undefined symbol reference
+            return "{}(undefined/n)".format(sc.name)
+
+        return '{}(={})'.format(sc.name, sc.str_value)
+
+    # For other items, use the standard format
+    return standard_sc_expr_str(sc)
+
+
+def _expr_str(expr):
+    # Custom expression printer that shows symbol values
+    return expr_str(expr, _name_and_val_str)
+
+
+def _is_num(name):
+    # Heuristic to see if a symbol name looks like a number, for nicer output
+    # when printing expressions. Things like 16 are actually symbol names, only
+    # they get their name as their value when the symbol is undefined.
+
+    try:
+        int(name)
+    except ValueError:
+        if not name.startswith(("0x", "0X")):
+            return False
+
+        try:
+            int(name, 16)
+        except ValueError:
+            return False
+
+    return True
+
+
+if __name__ == "__main__":
+    _main()

+ 0 - 1206
tools/pymenuconfig.py

@@ -1,1206 +0,0 @@
-# SPDX-License-Identifier: ISC
-# -*- coding: utf-8 -*-
-
-"""
-Overview
-========
-
-pymenuconfig is a small and simple frontend to Kconfiglib that's written
-entirely in Python using Tkinter as its GUI toolkit.
-
-Motivation
-==========
-
-Kconfig is a nice and powerful framework for build-time configuration and lots
-of projects already benefit from using it. Kconfiglib allows to utilize power of
-Kconfig by using scripts written in pure Python, without requiring one to build
-Linux kernel tools written in C (this can be quite tedious on anything that's
-not *nix). The aim of this project is to implement simple and small Kconfiglib
-GUI frontend that runs on as much systems as possible.
-
-Tkinter GUI toolkit is a natural choice if portability is considered, as it's
-a part of Python standard library and is available virtually in every CPython
-installation.
-
-
-User interface
-==============
-
-I've tried to replicate look and fill of Linux kernel 'menuconfig' tool that
-many users are used to, including keyboard-oriented control and textual
-representation of menus with fixed-width font.
-
-
-Usage
-=====
-
-The pymenuconfig module is executable and parses command-line args, so the
-most simple way to run menuconfig is to execute script directly:
-
-  python pymenuconfig.py --kconfig Kconfig
-
-As with most command-line tools list of options can be obtained with '--help':
-
-  python pymenuconfig.py --help
-
-If installed with setuptools, one can run it like this:
-
-  python -m pymenuconfig --kconfig Kconfig
-
-In case you're making a wrapper around menuconfig, you can either call main():
-
-  import pymenuconfig
-  pymenuconfig.main(['--kconfig', 'Kconfig'])
-
-Or import MenuConfig class, instantiate it and manually run Tkinter's mainloop:
-
-  import tkinter
-  import kconfiglib
-  from pymenuconfig import MenuConfig
-
-  kconfig = kconfiglib.Kconfig()
-  mconf = MenuConfig(kconfig)
-  tkinter.mainloop()
-
-"""
-
-from __future__ import print_function
-
-import os
-import sys
-import argparse
-import kconfiglib
-
-# Tk is imported differently depending on python major version
-if sys.version_info[0] < 3:
-    import Tkinter as tk
-    import tkFont as font
-    import tkFileDialog as filedialog
-    import tkMessageBox as messagebox
-else:
-    import tkinter as tk
-    from tkinter import font
-    from tkinter import filedialog
-    from tkinter import messagebox
-
-
-class ListEntry(object):
-    """
-    Represents visible menu node and holds all information related to displaying
-    menu node in a Listbox.
-
-    Instances of this class also handle all interaction with main window.
-    A node is displayed as a single line of text:
-      PREFIX INDENT BODY POSTFIX
-    - The PREFIX is always 3 characters or more and can take following values:
-      '   ' comment, menu, bool choice, etc.
-      Inside menus:
-      '< >' bool symbol has value 'n'
-      '<*>' bool symbol has value 'y'
-      '[ ]' tristate symbol has value 'n'
-      '[M]' tristate symbol has value 'm'
-      '[*]' tristate symbol has value 'y'
-      '- -' symbol has value 'n' that's not editable
-      '-M-' symbol has value 'm' that's not editable
-      '-*-' symbol has value 'y' that's not editable
-      '(M)' tristate choice has value 'm'
-      '(*)' tristate choice has value 'y'
-      '(some value)' value of non-bool/tristate symbols
-      Inside choices:
-      '( )' symbol has value 'n'
-      '(M)' symbol has value 'm'
-      '(*)' symbol has value 'y'
-    - INDENT is a sequence of space characters. It's used in implicit menus, and
-      adds 2 spaces for each nesting level
-    - BODY is a menu node prompt. '***' is added if node is a comment
-    - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable
-
-    Attributes:
-
-    node:
-      MenuNode instance this ListEntry is created for.
-
-    visible:
-      Whether entry should be shown in main window.
-
-    text:
-      String to display in a main window's Listbox.
-
-    refresh():
-      Updates .visible and .text attribute values.
-
-    set_tristate_value():
-      Set value for bool/tristate symbols, value should be one of 0,1,2 or None.
-      Usually it's called when user presses 'y', 'n', 'm' key.
-
-    set_str_value():
-      Set value for non-bool/tristate symbols, value is a string. Usually called
-      with a value returned by one of MenuConfig.ask_for_* methods.
-
-    toggle():
-      Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in
-      a main window. Also selects choice value.
-
-    select():
-      Called when '<Return>' key is pressed in a main window with 'SELECT'
-      action selected. Displays submenu, choice selection menu, or just selects
-      choice value. For non-bool/tristate symbols asks MenuConfig window to
-      handle value input via one of MenuConfig.ask_for_* methods.
-
-    show_help():
-      Called when '<Return>' key is pressed in a main window with 'HELP' action
-      selected. Prepares text help and calls MenuConfig.show_text() to display
-      text window.
-    """
-
-    # How to display value of BOOL and TRISTATE symbols
-    TRI_TO_DISPLAY = {
-        0: ' ',
-        1: 'M',
-        2: '*'
-    }
-
-    def __init__(self, mconf, node, indent):
-        self.indent = indent
-        self.node = node
-        self.menuconfig = mconf
-        self.visible = False
-        self.text = None
-
-    def __str__(self):
-        return self.text
-
-    def _is_visible(self):
-        node = self.node
-        v = True
-        v = v and node.prompt is not None
-        # It should be enough to check if prompt expression is not false and
-        # for menu nodes whether 'visible if' is not false
-        v = v and kconfiglib.expr_value(node.prompt[1]) > 0
-        if node.item == kconfiglib.MENU:
-            v = v and kconfiglib.expr_value(node.visibility) > 0
-        # If node references Symbol, then we also account for symbol visibility
-        # TODO: need to re-think whether this is needed
-        if isinstance(node.item, kconfiglib.Symbol):
-            if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
-                v = v and len(node.item.assignable) > 0
-            else:
-                v = v and node.item.visibility > 0
-        return v
-
-    def _get_text(self):
-        """
-        Compute textual representation of menu node (a line in ListView)
-        """
-        node = self.node
-        item = node.item
-        # Determine prefix
-        prefix = '   '
-        if (isinstance(item, kconfiglib.Symbol) and item.choice is None or
-            isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE):
-            # The node is for either a symbol outside of choice statement
-            # or a tristate choice
-            if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
-                value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
-                if len(item.assignable) > 1:
-                    # Symbol is editable
-                    if 1 in item.assignable:
-                        prefix = '<{}>'.format(value)
-                    else:
-                        prefix = '[{}]'.format(value)
-                else:
-                    # Symbol is not editable
-                    prefix = '-{}-'.format(value)
-            else:
-                prefix = '({})'.format(item.str_value)
-        elif isinstance(item, kconfiglib.Symbol) and item.choice is not None:
-            # The node is for symbol inside choice statement
-            if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
-                value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
-                if len(item.assignable) > 0:
-                    # Symbol is editable
-                    prefix = '({})'.format(value)
-                else:
-                    # Symbol is not editable
-                    prefix = '-{}-'.format(value)
-            else:
-                prefix = '({})'.format(item.str_value)
-
-        # Prefix should be at least 3 chars long
-        if len(prefix) < 3:
-            prefix += ' ' * (3 - len(prefix))
-        # Body
-        body = ''
-        if node.prompt is not None:
-            if item is kconfiglib.COMMENT:
-                body = '*** {} ***'.format(node.prompt[0])
-            else:
-                body = node.prompt[0]
-        # Suffix
-        is_menu = False
-        is_new = False
-        if (item is kconfiglib.MENU
-            or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig
-            or isinstance(item, kconfiglib.Choice)):
-            is_menu = True
-        if isinstance(item, kconfiglib.Symbol) and item.user_value is None:
-            is_new = True
-        # For symbol inside choice that has 'y' value, '(NEW)' is not displayed
-        if (isinstance(item, kconfiglib.Symbol)
-            and item.choice and item.choice.tri_value == 2):
-            is_new = False
-        # Choice selection - displayed only for choices which have 'y' value
-        choice_selection = None
-        if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y':
-            choice_selection = ''
-            if item.selection is not None:
-                sym = item.selection
-                if sym.nodes and sym.nodes[0].prompt is not None:
-                    choice_selection = sym.nodes[0].prompt[0]
-        text = '  {prefix} {indent}{body}{choice}{new}{menu}'.format(
-            prefix=prefix,
-            indent='  ' * self.indent,
-            body=body,
-            choice='' if choice_selection is None else ' ({})'.format(
-                choice_selection
-            ),
-            new=' (NEW)' if is_new else '',
-            menu=' --->' if is_menu else ''
-        )
-        return text
-
-    def refresh(self):
-        self.visible = self._is_visible()
-        self.text = self._get_text()
-
-    def set_tristate_value(self, value):
-        """
-        Call to change value of BOOL, TRISTATE symbols
-
-        It's preferred to use this instead of item.set_value as it handles
-        all necessary interaction with MenuConfig window when symbol value
-        changes
-
-        None value is accepted but ignored
-        """
-        item = self.node.item
-        if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
-            and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)
-            and value is not None):
-            if value in item.assignable:
-                item.set_value(value)
-            elif value == 2 and 1 in item.assignable:
-                print(
-                    'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name),
-                    file=sys.stderr
-                )
-                item.set_value(1)
-            self.menuconfig.mark_as_changed()
-            self.menuconfig.refresh_display()
-
-    def set_str_value(self, value):
-        """
-        Call to change value of HEX, INT, STRING symbols
-
-        It's preferred to use this instead of item.set_value as it handles
-        all necessary interaction with MenuConfig window when symbol value
-        changes
-
-        None value is accepted but ignored
-        """
-        item = self.node.item
-        if (isinstance(item, kconfiglib.Symbol)
-            and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)
-            and value is not None):
-            item.set_value(value)
-            self.menuconfig.mark_as_changed()
-            self.menuconfig.refresh_display()
-
-    def toggle(self):
-        """
-        Called when <space> key is pressed
-        """
-        item = self.node.item
-        if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
-            and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)):
-            value = item.tri_value
-            # Find next value in Symbol/Choice.assignable, or use assignable[0]
-            try:
-                it = iter(item.assignable)
-                while value != next(it):
-                    pass
-                self.set_tristate_value(next(it))
-            except StopIteration:
-                self.set_tristate_value(item.assignable[0])
-
-    def select(self):
-        """
-        Called when <Return> key is pressed and SELECT action is selected
-        """
-        item = self.node.item
-        # - Menu: dive into submenu
-        # - INT, HEX, STRING symbol: raise prompt to enter symbol value
-        # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value
-        if (item is kconfiglib.MENU
-            or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig
-            or isinstance(item, kconfiglib.Choice)):
-            # Dive into submenu
-            self.menuconfig.show_submenu(self.node)
-        elif (isinstance(item, kconfiglib.Symbol) and item.type in
-              (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)):
-            # Raise prompt to enter symbol value
-            ident = self.node.prompt[0] if self.node.prompt is not None else None
-            title = 'Symbol: {}'.format(item.name)
-            if item.type is kconfiglib.INT:
-                # Find enabled ranges
-                ranges = [
-                    (int(start.str_value), int(end.str_value))
-                    for start, end, expr in item.ranges
-                    if kconfiglib.expr_value(expr) > 0
-                ]
-                # Raise prompt
-                self.set_str_value(str(self.menuconfig.ask_for_int(
-                    ident=ident,
-                    title=title,
-                    value=item.str_value,
-                    ranges=ranges
-                )))
-            elif item.type is kconfiglib.HEX:
-                # Find enabled ranges
-                ranges = [
-                    (int(start.str_value, base=16), int(end.str_value, base=16))
-                    for start, end, expr in item.ranges
-                    if kconfiglib.expr_value(expr) > 0
-                ]
-                # Raise prompt
-                self.set_str_value(hex(self.menuconfig.ask_for_hex(
-                    ident=ident,
-                    title=title,
-                    value=item.str_value,
-                    ranges=ranges
-                )))
-            elif item.type is kconfiglib.STRING:
-                # Raise prompt
-                self.set_str_value(self.menuconfig.ask_for_string(
-                    ident=ident,
-                    title=title,
-                    value=item.str_value
-                ))
-        elif (isinstance(item, kconfiglib.Symbol)
-              and item.choice is not None and item.choice.tri_value == 2):
-            # Symbol inside choice -> set symbol value to 'y'
-            self.set_tristate_value(2)
-
-    def show_help(self):
-        node = self.node
-        item = self.node.item
-        if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)):
-            title = 'Help for symbol: {}'.format(item.name)
-            if node.help:
-                help = node.help
-            else:
-                help = 'There is no help available for this option.\n'
-            lines = []
-            lines.append(help)
-            lines.append(
-                'Symbol: {} [={}]'.format(
-                    item.name if item.name else '<UNNAMED>', item.str_value
-                )
-            )
-            lines.append('Type  : {}'.format(kconfiglib.TYPE_TO_STR[item.type]))
-            for n in item.nodes:
-                lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>'))
-                lines.append('  Defined at {}:{}'.format(n.filename, n.linenr))
-                lines.append('  Depends on: {}'.format(kconfiglib.expr_str(n.dep)))
-            text = '\n'.join(lines)
-        else:
-            title = 'Help'
-            text = 'Help not available for this menu node.\n'
-        self.menuconfig.show_text(text, title)
-        self.menuconfig.refresh_display()
-
-
-class EntryDialog(object):
-    """
-    Creates modal dialog (top-level Tk window) with labels, entry box and two
-    buttons: OK and CANCEL.
-    """
-    def __init__(self, master, text, title, ident=None, value=None):
-        self.master = master
-        dlg = self.dlg = tk.Toplevel(master)
-        self.dlg.withdraw() #hiden window
-        dlg.title(title)
-        # Identifier label
-        if ident is not None:
-            self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
-            self.label_id['font'] = font.nametofont('TkFixedFont')
-            self.label_id['text'] = '# {}'.format(ident)
-            self.label_id.pack(fill=tk.X, padx=2, pady=2)
-        # Label
-        self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
-        self.label['font'] = font.nametofont('TkFixedFont')
-        self.label['text'] = text
-        self.label.pack(fill=tk.X, padx=10, pady=4)
-        # Entry box
-        self.entry = tk.Entry(dlg)
-        self.entry['font'] = font.nametofont('TkFixedFont')
-        self.entry.pack(fill=tk.X, padx=2, pady=2)
-        # Frame for buttons
-        self.frame = tk.Frame(dlg)
-        self.frame.pack(padx=2, pady=2)
-        # Button
-        self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
-        self.btn_accept['font'] = font.nametofont('TkFixedFont')
-        self.btn_accept.pack(side=tk.LEFT, padx=2)
-        self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
-        self.btn_cancel['font'] = font.nametofont('TkFixedFont')
-        self.btn_cancel.pack(side=tk.LEFT, padx=2)
-        # Bind Enter and Esc keys
-        self.dlg.bind('<Return>', self.accept)
-        self.dlg.bind('<Escape>', self.cancel)
-        # Dialog is resizable only by width
-        self.dlg.resizable(1, 0)
-        # Set supplied value (if any)
-        if value is not None:
-            self.entry.insert(0, value)
-            self.entry.selection_range(0, tk.END)
-        # By default returned value is None. To caller this means that entry
-        # process was cancelled
-        self.value = None
-        # Modal dialog
-        dlg.transient(master)
-        dlg.grab_set()
-        # Center dialog window
-        _center_window_above_parent(master, dlg)
-        self.dlg.deiconify() # show window
-        # Focus entry field
-        self.entry.focus_set()
-
-    def accept(self, ev=None):
-        self.value = self.entry.get()
-        self.dlg.destroy()
-
-    def cancel(self, ev=None):
-        self.dlg.destroy()
-
-
-class TextDialog(object):
-    def __init__(self, master, text, title):
-        self.master = master
-        dlg = self.dlg = tk.Toplevel(master)
-        self.dlg.withdraw() #hiden window
-        dlg.title(title)
-        dlg.minsize(600,400)
-        # Text
-        self.text = tk.Text(dlg, height=1)
-        self.text['font'] = font.nametofont('TkFixedFont')
-        self.text.insert(tk.END, text)
-        # Make text read-only
-        self.text['state'] = tk.DISABLED
-        self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
-        # Frame for buttons
-        self.frame = tk.Frame(dlg)
-        self.frame.pack(padx=2, pady=2)
-        # Button
-        self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
-        self.btn_accept['font'] = font.nametofont('TkFixedFont')
-        self.btn_accept.pack(side=tk.LEFT, padx=2)
-        # Bind Enter and Esc keys
-        self.dlg.bind('<Return>', self.accept)
-        self.dlg.bind('<Escape>', self.cancel)
-        # Modal dialog
-        dlg.transient(master)
-        dlg.grab_set()
-        # Center dialog window
-        _center_window_above_parent(master, dlg)
-        self.dlg.deiconify() # show window
-        # Focus entry field
-        self.text.focus_set()
-
-    def accept(self, ev=None):
-        self.dlg.destroy()
-
-    def cancel(self, ev=None):
-        self.dlg.destroy()
-
-
-class MenuConfig(object):
-    (
-        ACTION_SELECT,
-        ACTION_EXIT,
-        ACTION_HELP,
-        ACTION_LOAD,
-        ACTION_SAVE,
-        ACTION_SAVE_AS
-    ) = range(6)
-
-    ACTIONS = (
-        ('Select', ACTION_SELECT),
-        ('Exit', ACTION_EXIT),
-        ('Help', ACTION_HELP),
-        ('Load', ACTION_LOAD),
-        ('Save', ACTION_SAVE),
-        ('Save as', ACTION_SAVE_AS),
-    )
-
-    def __init__(self, kconfig, __silent=None):
-        self.kconfig = kconfig
-        self.__silent = __silent
-        if self.__silent is True:
-            return
-
-        # Instantiate Tk widgets
-        self.root = tk.Tk()
-        self.root.withdraw() #hiden window
-        dlg = self.root
-
-        # Window title
-        dlg.title('pymenuconfig')
-        # Some empirical window size
-        dlg.minsize(500, 300)
-        dlg.geometry('800x600')
-
-        # Label that shows position in menu tree
-        self.label_position = tk.Label(
-            dlg,
-            anchor=tk.W,
-            justify=tk.LEFT,
-            font=font.nametofont('TkFixedFont')
-        )
-        self.label_position.pack(fill=tk.X, padx=2)
-
-        # 'Tip' frame and text
-        self.frame_tip = tk.LabelFrame(
-            dlg,
-            text='Tip'
-        )
-        self.label_tip = tk.Label(
-            self.frame_tip,
-            anchor=tk.W,
-            justify=tk.LEFT,
-            font=font.nametofont('TkFixedFont')
-        )
-        self.label_tip['text'] = '\n'.join([
-            'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
-            'Pressing <Y> includes, <N> excludes, <M> modularizes features',
-            'Press <Esc> to go one level up. Press <Esc> at top level to exit',
-            'Legend: [*] built-in  [ ] excluded  <M> module  < > module capable'
-        ])
-        self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
-        self.frame_tip.pack(fill=tk.X, padx=2)
-
-        # Main ListBox where all the magic happens
-        self.list = tk.Listbox(
-            dlg,
-            selectmode=tk.SINGLE,
-            activestyle=tk.NONE,
-            font=font.nametofont('TkFixedFont'),
-            height=1,
-        )
-        self.list['foreground'] = 'Blue'
-        self.list['background'] = 'Gray95'
-        # Make selection invisible
-        self.list['selectbackground'] = self.list['background']
-        self.list['selectforeground'] = self.list['foreground']
-        self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
-
-        # Frame with radio buttons
-        self.frame_radio = tk.Frame(dlg)
-        self.radio_buttons = []
-        self.tk_selected_action = tk.IntVar()
-        for text, value in MenuConfig.ACTIONS:
-            btn = tk.Radiobutton(
-                self.frame_radio,
-                variable=self.tk_selected_action,
-                value=value
-            )
-            btn['text'] = '< {} >'.format(text)
-            btn['font'] = font.nametofont('TkFixedFont')
-            btn['indicatoron'] = 0
-            btn.pack(side=tk.LEFT)
-            self.radio_buttons.append(btn)
-        self.frame_radio.pack(anchor=tk.CENTER, pady=4)
-        # Label with status information
-        self.tk_status = tk.StringVar()
-        self.label_status = tk.Label(
-            dlg,
-            textvariable=self.tk_status,
-            anchor=tk.W,
-            justify=tk.LEFT,
-            font=font.nametofont('TkFixedFont')
-        )
-        self.label_status.pack(fill=tk.X, padx=4, pady=4)
-        # Center window
-        _center_window(self.root, dlg)
-        self.root.deiconify() # show window
-        # Disable keyboard focus on all widgets ...
-        self._set_option_to_all_children(dlg, 'takefocus', 0)
-        # ... except for main ListBox
-        self.list['takefocus'] = 1
-        self.list.focus_set()
-        # Bind keys
-        dlg.bind('<Escape>', self.handle_keypress)
-        dlg.bind('<space>', self.handle_keypress)
-        dlg.bind('<Return>', self.handle_keypress)
-        dlg.bind('<Right>', self.handle_keypress)
-        dlg.bind('<Left>', self.handle_keypress)
-        dlg.bind('<Up>', self.handle_keypress)
-        dlg.bind('<Down>', self.handle_keypress)
-        dlg.bind('n', self.handle_keypress)
-        dlg.bind('m', self.handle_keypress)
-        dlg.bind('y', self.handle_keypress)
-        # Register callback that's called when window closes
-        dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
-        # Init fields
-        self.node = None
-        self.node_stack = []
-        self.all_entries = []
-        self.shown_entries = []
-        self.config_path = None
-        self.unsaved_changes = False
-        self.status_string = 'NEW CONFIG'
-        self.update_status()
-        # Display first child of top level node (the top level node is 'mainmenu')
-        self.show_node(self.kconfig.top_node)
-
-    def _set_option_to_all_children(self, widget, option, value):
-        widget[option] = value
-        for n,c in widget.children.items():
-            self._set_option_to_all_children(c, option, value)
-
-    def _invert_colors(self, idx):
-        self.list.itemconfig(idx, {'bg' : self.list['foreground']})
-        self.list.itemconfig(idx, {'fg' : self.list['background']})
-
-    @property
-    def _selected_entry(self):
-        # type: (...) -> ListEntry
-        active_idx = self.list.index(tk.ACTIVE)
-        if active_idx >= 0 and active_idx < len(self.shown_entries):
-            return self.shown_entries[active_idx]
-        return None
-
-    def _select_node(self, node):
-        # type: (kconfiglib.MenuNode) -> None
-        """
-        Attempts to select entry that corresponds to given MenuNode in main listbox
-        """
-        idx = None
-        for i, e in enumerate(self.shown_entries):
-            if e.node is node:
-                idx = i
-                break
-        if idx is not None:
-            self.list.activate(idx)
-            self.list.see(idx)
-            self._invert_colors(idx)
-
-    def handle_keypress(self, ev):
-        keysym = ev.keysym
-        if keysym == 'Left':
-            self._select_action(prev=True)
-        elif keysym == 'Right':
-            self._select_action(prev=False)
-        elif keysym == 'Up':
-            self.refresh_display(reset_selection=False)
-        elif keysym == 'Down':
-            self.refresh_display(reset_selection=False)
-        elif keysym == 'space':
-            self._selected_entry.toggle()
-        elif keysym in ('n', 'm', 'y'):
-            self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
-        elif keysym == 'Return':
-            action = self.tk_selected_action.get()
-            if action == self.ACTION_SELECT:
-                self._selected_entry.select()
-            elif action == self.ACTION_EXIT:
-                self._action_exit()
-            elif action == self.ACTION_HELP:
-                self._selected_entry.show_help()
-            elif action == self.ACTION_LOAD:
-                if self.prevent_losing_changes():
-                    self.open_config()
-            elif action == self.ACTION_SAVE:
-                self.save_config()
-            elif action == self.ACTION_SAVE_AS:
-                self.save_config(force_file_dialog=True)
-        elif keysym == 'Escape':
-            self._action_exit()
-        pass
-
-    def _close_window(self):
-        if self.prevent_losing_changes():
-            print('Exiting..')
-            if self.__silent is True:
-                return
-            self.root.destroy()
-
-    def _action_exit(self):
-        if self.node_stack:
-            self.show_parent()
-        else:
-            self._close_window()
-
-    def _select_action(self, prev=False):
-        # Determine the radio button that's activated
-        action = self.tk_selected_action.get()
-        if prev:
-            action -= 1
-        else:
-            action += 1
-        action %= len(MenuConfig.ACTIONS)
-        self.tk_selected_action.set(action)
-
-    def _collect_list_entries(self, start_node, indent=0):
-        """
-        Given first MenuNode of nodes list at some level in menu hierarchy,
-        collects nodes that may be displayed when viewing and editing that
-        hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
-        on 'config' entry via 'if' statement which are internally represented
-        as children of their dependency
-        """
-        entries = []
-        n = start_node
-        while n is not None:
-            entries.append(ListEntry(self, n, indent))
-            # If node refers to a symbol (X) and has children, it is either
-            # 'config' or 'menuconfig'. The children are items inside 'if X'
-            # block that immediately follows 'config' or 'menuconfig' entry.
-            # If it's a 'menuconfig' then corresponding MenuNode is shown as a
-            # regular menu entry. But if it's a 'config', then its children need
-            # to be shown in the same list with their texts indented
-            if (n.list is not None
-                and isinstance(n.item, kconfiglib.Symbol)
-                and n.is_menuconfig == False):
-                entries.extend(
-                    self._collect_list_entries(n.list, indent=indent + 1)
-                )
-            n = n.next
-        return entries
-
-    def refresh_display(self, reset_selection=False):
-        # Refresh list entries' attributes
-        for e in self.all_entries:
-            e.refresh()
-        # Try to preserve selection upon refresh
-        selected_entry = self._selected_entry
-        # Also try to preserve listbox scroll offset
-        # If not preserved, the see() method will make wanted item to appear
-        # at the bottom of the list, even if previously it was in center
-        scroll_offset = self.list.yview()[0]
-        # Show only visible entries
-        self.shown_entries = [e for e in self.all_entries if e.visible]
-        # Refresh listbox contents
-        self.list.delete(0, tk.END)
-        self.list.insert(0, *self.shown_entries)
-        if selected_entry and not reset_selection:
-            # Restore scroll position
-            self.list.yview_moveto(scroll_offset)
-            # Activate previously selected node
-            self._select_node(selected_entry.node)
-        else:
-            # Select the topmost entry
-            self.list.activate(0)
-            self._invert_colors(0)
-        # Select ACTION_SELECT on each refresh (mimic C menuconfig)
-        self.tk_selected_action.set(self.ACTION_SELECT)
-        # Display current location in configuration tree
-        pos = []
-        for n in self.node_stack + [self.node]:
-            pos.append(n.prompt[0] if n.prompt else '[none]')
-        self.label_position['text'] = u'# ' + u' -> '.join(pos)
-
-    def show_node(self, node):
-        self.node = node
-        if node.list is not None:
-            self.all_entries = self._collect_list_entries(node.list)
-        else:
-            self.all_entries = []
-        self.refresh_display(reset_selection=True)
-
-    def show_submenu(self, node):
-        self.node_stack.append(self.node)
-        self.show_node(node)
-
-    def show_parent(self):
-        if self.node_stack:
-            select_node = self.node
-            parent_node = self.node_stack.pop()
-            self.show_node(parent_node)
-            # Restore previous selection
-            self._select_node(select_node)
-            self.refresh_display(reset_selection=False)
-
-    def ask_for_string(self, ident=None, title='Enter string', value=None):
-        """
-        Raises dialog with text entry widget and asks user to enter string
-
-        Return:
-            - str - user entered string
-            - None - entry was cancelled
-        """
-        text = 'Please enter a string value\n' \
-               'User <Enter> key to accept the value\n' \
-               'Use <Esc> key to cancel entry\n'
-        d = EntryDialog(self.root, text, title, ident=ident, value=value)
-        self.root.wait_window(d.dlg)
-        self.list.focus_set()
-        return d.value
-
-    def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
-        """
-        Raises dialog with text entry widget and asks user to enter decimal number
-        Ranges should be iterable of tuples (start, end),
-        where 'start' and 'end' specify allowed value range (inclusively)
-
-        Return:
-            - int - when valid number that falls within any one of specified ranges is entered
-            - None - invalid number or entry was cancelled
-        """
-        text = 'Please enter a decimal value. Fractions will not be accepted\n' \
-               'User <Enter> key to accept the value\n' \
-               'Use <Esc> key to cancel entry\n'
-        d = EntryDialog(self.root, text, title, ident=ident, value=value)
-        self.root.wait_window(d.dlg)
-        self.list.focus_set()
-        ivalue = None
-        if d.value:
-            try:
-                ivalue = int(d.value)
-            except ValueError:
-                messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
-            if ivalue is not None and ranges:
-                allowed = False
-                for start, end in ranges:
-                    allowed = allowed or start <= ivalue and ivalue <= end
-                if not allowed:
-                    messagebox.showerror(
-                        'Bad value',
-                        'Entered value \'{:d}\' is out of range\n'
-                        'Allowed:\n{}'.format(
-                            ivalue,
-                            '\n'.join(['  {:d} - {:d}'.format(s,e) for s,e in ranges])
-                        )
-                    )
-                    ivalue = None
-        return ivalue
-
-    def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
-        """
-        Raises dialog with text entry widget and asks user to enter decimal number
-        Ranges should be iterable of tuples (start, end),
-        where 'start' and 'end' specify allowed value range (inclusively)
-
-        Return:
-            - int - when valid number that falls within any one of specified ranges is entered
-            - None - invalid number or entry was cancelled
-        """
-        text = 'Please enter a hexadecimal value\n' \
-               'User <Enter> key to accept the value\n' \
-               'Use <Esc> key to cancel entry\n'
-        d = EntryDialog(self.root, text, title, ident=ident, value=value)
-        self.root.wait_window(d.dlg)
-        self.list.focus_set()
-        hvalue = None
-        if d.value:
-            try:
-                hvalue = int(d.value, base=16)
-            except ValueError:
-                messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
-            if hvalue is not None and ranges:
-                allowed = False
-                for start, end in ranges:
-                    allowed = allowed or start <= hvalue and hvalue <= end
-                if not allowed:
-                    messagebox.showerror(
-                        'Bad value',
-                        'Entered value \'0x{:x}\' is out of range\n'
-                        'Allowed:\n{}'.format(
-                            hvalue,
-                            '\n'.join(['  0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
-                        )
-                    )
-                    hvalue = None
-        return hvalue
-
-    def show_text(self, text, title='Info'):
-        """
-        Raises dialog with read-only text view that contains supplied text
-        """
-        d = TextDialog(self.root, text, title)
-        self.root.wait_window(d.dlg)
-        self.list.focus_set()
-
-    def mark_as_changed(self):
-        """
-        Marks current config as having unsaved changes
-        Should be called whenever config value is changed
-        """
-        self.unsaved_changes = True
-        self.update_status()
-
-    def set_status_string(self, status):
-        """
-        Sets status string displayed at the bottom of the window
-        """
-        self.status_string = status
-        self.update_status()
-
-    def update_status(self):
-        """
-        Updates status bar display
-        Status bar displays:
-        - unsaved status
-        - current config path
-        - status string (see set_status_string())
-        """
-        if self.__silent is True:
-            return
-        self.tk_status.set('{} [{}] {}'.format(
-            '<UNSAVED>' if self.unsaved_changes else '',
-            self.config_path if self.config_path else '',
-            self.status_string
-        ))
-
-    def _check_is_visible(self, node):
-        v = True
-        v = v and node.prompt is not None
-        # It should be enough to check if prompt expression is not false and
-        # for menu nodes whether 'visible if' is not false
-        v = v and kconfiglib.expr_value(node.prompt[1]) > 0
-        if node.item == kconfiglib.MENU:
-            v = v and kconfiglib.expr_value(node.visibility) > 0
-        # If node references Symbol, then we also account for symbol visibility
-        # TODO: need to re-think whether this is needed
-        if isinstance(node.item, kconfiglib.Symbol):
-            if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
-                v = v and len(node.item.assignable) > 0
-            else:
-                v = v and node.item.visibility > 0
-        return v
-
-    def config_is_changed(self):
-        is_changed = False
-        node = self.kconfig.top_node.list
-        if not node:
-            # Empty configuration
-            return is_changed
-
-        while 1:
-            item = node.item
-            if isinstance(item, kconfiglib.Symbol) and item.user_value is None and self._check_is_visible(node):
-                is_changed = True
-                print("Config \"# {}\" has changed, need save config file\n".format(node.prompt[0]))
-                break;
-
-            # Iterative tree walk using parent pointers
-
-            if node.list:
-                node = node.list
-            elif node.next:
-                node = node.next
-            else:
-                while node.parent:
-                    node = node.parent
-                    if node.next:
-                        node = node.next
-                        break
-                else:
-                    break
-        return is_changed
-
-    def prevent_losing_changes(self):
-        """
-        Checks if there are unsaved changes and asks user to save or discard them
-        This routine should be called whenever current config is going to be discarded
-
-        Raises the usual 'Yes', 'No', 'Cancel' prompt.
-
-        Return:
-            - True: caller may safely drop current config state
-            - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
-        """
-        if self.config_is_changed() == True:
-            self.mark_as_changed()
-        if not self.unsaved_changes:
-            return True
-        
-        if self.__silent:
-            saved = self.save_config()
-            return saved
-        res = messagebox.askyesnocancel(
-            parent=self.root,
-            title='Unsaved changes',
-            message='Config has unsaved changes. Do you want to save them?'
-        )
-        if res is None:
-            return False
-        elif res is False:
-            return True
-        # Otherwise attempt to save config and succeed only if config has been saved successfully
-        saved = self.save_config()
-        return saved
-
-    def open_config(self, path=None):
-        if path is None:
-            # Create open dialog. Either existing file is selected or no file is selected as a result
-            path = filedialog.askopenfilename(
-                parent=self.root,
-                title='Open config..',
-                initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
-                filetypes=(('.config files', '*.config'), ('All files', '*.*'))
-            )
-            if not path or not os.path.isfile(path):
-                return False
-        path = os.path.abspath(path)
-        print('Loading config: \'{}\''.format(path))
-        # Try to open given path
-        # If path does not exist, we still set current config path to it but don't load anything
-        self.unsaved_changes = False
-        self.config_path = path
-        if not os.path.exists(path):
-            self.set_status_string('New config')
-            self.mark_as_changed()
-            return True
-        # Load config and set status accordingly
-        try:
-            self.kconfig.load_config(path)
-        except IOError as e:
-            self.set_status_string('Failed to load: \'{}\''.format(path))
-            if not self.__silent:
-                self.refresh_display()
-            print('Failed to load config \'{}\': {}'.format(path, e))
-            return False
-        self.set_status_string('Opened config')
-        if not self.__silent:
-            self.refresh_display()
-        return True
-
-    def save_config(self, force_file_dialog=False):
-        path = self.config_path
-        if path is None or force_file_dialog:
-            path = filedialog.asksaveasfilename(
-                parent=self.root,
-                title='Save config as..',
-                initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
-                initialfile=os.path.basename(self.config_path) if self.config_path else None,
-                defaultextension='.config',
-                filetypes=(('.config files', '*.config'), ('All files', '*.*'))
-            )
-        if not path:
-            return False
-        path = os.path.abspath(path)
-        print('Saving config: \'{}\''.format(path))
-        # Try to save config to selected path
-        try:
-            self.kconfig.write_config(path, header="#\n# Automatically generated file; DO NOT EDIT.\n")
-            self.unsaved_changes = False
-            self.config_path = path
-            self.set_status_string('Saved config')
-        except IOError as e:
-            self.set_status_string('Failed to save: \'{}\''.format(path))
-            print('Save failed: {}'.format(e), file=sys.stderr)
-            return False
-        return True
-
-
-def _center_window(root, window):
-    # type: (tk.Tk, tk.Toplevel) -> None
-    """
-    Attempts to center window on screen
-    """
-    root.update_idletasks()
-    # root.eval('tk::PlaceWindow {!s} center'.format(
-    #     window.winfo_pathname(window.winfo_id())
-    # ))
-    w = window.winfo_width()
-    h = window.winfo_height()
-    ws = window.winfo_screenwidth()
-    hs = window.winfo_screenheight()
-    x = (ws / 2) - (w / 2)
-    y = (hs / 2) - (h / 2)
-    window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
-    window.lift()
-    window.focus_force()
-
-
-def _center_window_above_parent(root, window):
-    # type: (tk.Tk, tk.Toplevel) -> None
-    """
-    Attempts to center window above its parent window
-    """
-    # root.eval('tk::PlaceWindow {!s} center'.format(
-    #     window.winfo_pathname(window.winfo_id())
-    # ))
-    root.update_idletasks()
-    parent = window.master
-    w = window.winfo_width()
-    h = window.winfo_height()
-    px = parent.winfo_rootx()
-    py = parent.winfo_rooty()
-    pw = parent.winfo_width()
-    ph = parent.winfo_height()
-    x = px + (pw / 2) - (w / 2)
-    y = py + (ph / 2) - (h / 2)
-    window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
-    window.lift()
-    window.focus_force()
-
-
-def main(argv=None):
-    if argv is None:
-        argv = sys.argv[1:]
-    # Instantiate cmd options parser
-    parser = argparse.ArgumentParser(
-        description='Interactive Kconfig configuration editor'
-    )
-    parser.add_argument(
-        '--kconfig',
-        metavar='FILE',
-        type=str,
-        default='Kconfig',
-        help='path to root Kconfig file'
-    )
-    parser.add_argument(
-        '--config',
-        metavar='FILE',
-        type=str,
-        help='path to .config file to load'
-    )
-    if "--silent" in argv:
-        parser.add_argument(
-            '--silent',
-            dest = '_silent_',
-            type=str,
-            help='silent mode, not show window'
-        )
-    args = parser.parse_args(argv)
-    kconfig_path = args.kconfig
-    config_path = args.config
-    # Verify that Kconfig file exists
-    if not os.path.isfile(kconfig_path):
-        raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
-
-    # Parse Kconfig files
-    kconf = kconfiglib.Kconfig(filename=kconfig_path)
-
-    if "--silent" not in argv:
-        print("In normal mode. Will show menuconfig window.")
-        mc = MenuConfig(kconf)
-        # If config file was specified, load it
-        if config_path:
-            mc.open_config(config_path)
-
-        print("Enter mainloop. Waiting...")
-        tk.mainloop()
-    else:
-        print("In silent mode. Don`t show menuconfig window.")
-        mc = MenuConfig(kconf, True)
-        # If config file was specified, load it
-        if config_path:
-            mc.open_config(config_path)
-        mc._close_window()
-
-
-if __name__ == '__main__':
-    main()

Some files were not shown because too many files changed in this diff