فهرست منبع

[tools] add menuconfig GUI by python.

armink 6 سال پیش
والد
کامیت
244c1a810d
4فایلهای تغییر یافته به همراه2833 افزوده شده و 208 حذف شده
  1. 11 1
      tools/building.py
  2. 491 207
      tools/kconfiglib.py
  3. 18 0
      tools/menuconfig.py
  4. 2313 0
      tools/pyguiconfig.py

+ 11 - 1
tools/building.py

@@ -377,12 +377,17 @@ 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 ASCII menuconfig for RT-Thread BSP')
     AddOption('--pyconfig-silent',
                 dest = 'pyconfig_silent',
                 action = 'store_true',
                 default = False,
                 help = 'Don`t show pyconfig window')
+    AddOption('--guiconfig',
+                dest = 'guiconfig',
+                action = 'store_true',
+                default = False,
+                help = 'Python GUI menuconfig for RT-Thread BSP')
 
     if GetOption('pyconfig_silent'):    
         from menuconfig import pyconfig_silent
@@ -394,6 +399,11 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
 
         pyconfig(Rtt_Root)
         exit(0)
+    elif GetOption('guiconfig'):
+        from menuconfig import guiconfig
+
+        guiconfig(Rtt_Root)
+        exit(0)
 
     configfn = GetOption('useconfig')
     if configfn:

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 491 - 207
tools/kconfiglib.py


+ 18 - 0
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
@@ -269,3 +270,20 @@ def pyconfig_silent(RTT_ROOT):
 
     # silent mode, force to make rtconfig.h
     mk_rtconfig(fn)
+
+# guiconfig for windows and linux
+def guiconfig(RTT_ROOT):
+    import pyguiconfig
+
+    touch_env()
+    env_dir = get_env_dir()
+
+    os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages')
+
+    fn = '.config'
+
+    sys.argv = ['guiconfig', 'Kconfig'];
+    pyguiconfig._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()

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است