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