source: menu/menuconfig.py@ 2c603d8

ablfs-more legacy trunk
Last change on this file since 2c603d8 was f596dde, checked in by Pierre Labastie <pierre@…>, 6 years ago

Get rid of the GPLv2 license:

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