source: menu/menuconfig.py@ 83cfe91

trunk
Last change on this file since 83cfe91 was cf2f109, checked in by Pierre Labastie <pierre@…>, 6 years ago

Update to Kconfiglib version 12.4.0. This removes the need to use
".configuration.old"

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