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