#!/usr/bin/env python3 # Copyright (c) 2018, Nordic Semiconductor ASA and Ulf Magnusson # SPDX-License-Identifier: ISC """ Overview ======== A curses-based menuconfig implementation. The interface should feel familiar to people used to mconf ('make menuconfig'). Supports the same keys as mconf, and also supports a set of keybindings inspired by Vi: J/K : Down/Up L : Enter menu/Toggle item H : Leave menu Ctrl-D/U: Page Down/Page Down G/End : Jump to end of list g/Home : Jump to beginning of list The mconf feature where pressing a key jumps to a menu entry with that character in it in the current menu isn't supported. A jump-to feature for jumping directly to any symbol (including invisible symbols) is available instead. Space and Enter are "smart" and try to do what you'd expect for the given menu entry. Running ======= menuconfig.py can be run either as a standalone executable or by calling the menu.menuconfig() function with an existing Kconfig instance. The second option is a bit inflexible in that it will still load and save .config, etc. When run in standalone mode, the top-level Kconfig file to load can be passed as a command-line argument. With no argument, it defaults to "Kconfig". The KCONFIG_CONFIG environment variable specifies the .config file to load (if it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. $srctree is supported through Kconfiglib. Color schemes ============= It is possible to customize the color scheme by setting the MENUCONFIG_STYLE environment variable. For example, setting it to 'aquatic' will enable an alternative, less yellow, more 'make menuconfig'-like color scheme, contributed by Mitja Horvat (pinkfluid). This is the current list of built-in styles: - default classic Kconfiglib theme with a yellow accent - monochrome colorless theme (uses only bold and standout) attributes, this style is used if the terminal doesn't support colors - aquatic blue tinted style loosely resembling the lxdialog theme It is possible to customize the current style by changing colors of UI elements on the screen. This is the list of elements that can be stylized: - path Top row in the main display, with the menu path - separator Separator lines between windows. Also used for the top line in the symbol - list List of items, e.g. the main display - selection Style for the selected item - inv-list: Like list, but for invisible items. Used in show-all mode. - inv-selection Like selection, but for invisible items. Used in show-all mode. - help Help text windows at the bottom of various fullscreen dialogs - frame Frame around dialog boxes - body Body of dialog boxes - edit Edit box in pop-up dialogs - jump-edit Edit box in jump-to dialog - text Symbol information text The color definition is a comma separated list of attributes: - fg:COLOR Set the foreground/background colors. COLOR can be one of * or * the basic 16 colors (black, red, green, yellow, blue, - bg:COLOR magenta,cyan, white and brighter versions, for example, brightred). On terminals that support more than 8 colors, you can also directly put in a color number, e.g. fg:123 (hexadecimal and octal constants are accepted as well). Colors outside the range -1..curses.COLORS-1 (which is terminal-dependent) are ignored (with a warning). The COLOR can be also specified using a RGB value in the HTML notation, for example #RRGGBB. If the terminal supports color changing, the color is render accurately. Otherwise the visually nearest color is used. If the background or foreground color of an element is not specified, it defaults to -1, representing the default terminal foreground or background color. Note: On some terminals a bright version of the color implies bold. - bold Use bold text - underline Use underline text - standout Standout text attribute (reverse color) More often than not, some UI elements share the same color definition. In such cases the right value may specify an UI element from which the color definition will be copied. For example, "separator=help" will apply the current color definition for "help" to "separator". A keyword without the '=' is assumed to be a style template. The template name is looked up in the built-in styles list and the style definition is expanded in-place. With this, built-in styles can be used as basis for new styles. For example, take the aquatic theme and give it a red selection bar: MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" If there's an error in the style definition or if a missing style is assigned to, the assignment will be ignored, along with a warning being printed on stderr. The 'default' theme is always implicitly parsed first (or the 'monochrome' theme if the terminal lacks colors), so the following two settings have the same effect: MENUCONFIG_STYLE="selection=fg:white,bg:red" MENUCONFIG_STYLE="default selection=fg:white,bg:red" Other features ============== - Seamless terminal resizing - No dependencies on *nix, as the 'curses' module is in the Python standard library - Unicode text entry - Improved information screen compared to mconf: * Expressions are split up by their top-level &&/|| operands to improve readability * Undefined symbols in expressions are pointed out * Menus and comments have information displays * Kconfig definitions are printed * The include path is shown, listing the locations of the 'source' statements that included the Kconfig file of the symbol (or other item) Limitations =========== - Python 3 only This is mostly due to Python 2 not having curses.get_wch(), which is needed for Unicode support. - Doesn't work out of the box on Windows Has been tested to work with the wheels provided at https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses though. """ import curses import errno import locale import os import platform import re import sys import textwrap from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \ AND, OR, NOT, \ expr_str, expr_value, split_expr, \ standard_sc_expr_str, \ TRI_TO_STR, TYPE_TO_STR, \ standard_kconfig, standard_config_filename # # Configuration variables # # If True, try to convert LC_CTYPE to a UTF-8 locale if it is set to the C # locale (which implies ASCII). This fixes curses Unicode I/O issues on systems # with bad defaults. ncurses configures itself from the locale settings. # # Related PEP: https://www.python.org/dev/peps/pep-0538/ _CONVERT_C_LC_CTYPE_TO_UTF8 = True # How many steps an implicit submenu will be indented. Implicit submenus are # created when an item depends on the symbol before it. Note that symbols # defined with 'menuconfig' create a separate menu instead of indenting. _SUBMENU_INDENT = 4 # Number of steps for Page Up/Down to jump _PG_JUMP = 6 # How far the cursor needs to be from the edge of the window before it starts # to scroll. Used for the main menu display, the information display, the # search display, and for text boxes. _SCROLL_OFFSET = 5 # Minimum width of dialogs that ask for text input _INPUT_DIALOG_MIN_WIDTH = 30 # Number of arrows pointing up/down to draw when a window is scrolled _N_SCROLL_ARROWS = 14 # Lines of help text shown at the bottom of the "main" display _MAIN_HELP_LINES = """ [Space/Enter] Toggle/enter [ESC] Leave menu [S] Save [O] Load [?] Symbol info [/] Jump to symbol [A] Toggle show-all mode [C] Toggle show-name mode [Q] Quit (prompts for save) [D] Save minimal config (advanced) """[1:-1].split("\n") # Lines of help text shown at the bottom of the information dialog _INFO_HELP_LINES = """ [ESC/q] Return to menu """[1:-1].split("\n") # Lines of help text shown at the bottom of the search dialog _JUMP_TO_HELP_LINES = """ Type text to narrow the search. Regexes are supported (via Python's 're' module). The up/down cursor keys step in the list. [Enter] jumps to the selected symbol. [ESC] aborts the search. Type multiple space-separated strings/regexes to find entries that match all of them. Type Ctrl-F to view the help of the selected item without leaving the dialog. """[1:-1].split("\n") _STYLES = { "default": """ path=fg:black,bg:white,bold separator=fg:black,bg:yellow,bold list=fg:black,bg:white selection=fg:white,bg:blue,bold inv-list=fg:red,bg:white inv-selection=fg:red,bg:blue help=fg:black,bg:white,bold frame=fg:black,bg:yellow,bold body=fg:white,bg:black edit=fg:white,bg:blue jump-edit=fg:white,bg:blue text=fg:black,bg:white """, # This style is forced on terminals that do no support colors "monochrome": """ path=bold separator=bold,standout list= selection=bold,standout inv-list=bold inv-selection=bold,standout help=bold frame=bold,standout body= edit=standout jump-edit= text= """, # Blue tinted style loosely resembling lxdialog "aquatic": """ path=fg:cyan,bg:blue,bold separator=fg:white,bg:cyan,bold help=fg:cyan,bg:blue,bold frame=fg:white,bg:cyan,bold body=fg:brightwhite,bg:blue edit=fg:black,bg:white """ } # Standard colors definition _STYLE_STD_COLORS = { # Basic colors "black": curses.COLOR_BLACK, "red": curses.COLOR_RED, "green": curses.COLOR_GREEN, "yellow": curses.COLOR_YELLOW, "blue": curses.COLOR_BLUE, "magenta": curses.COLOR_MAGENTA, "cyan": curses.COLOR_CYAN, "white": curses.COLOR_WHITE, # Bright versions "brightblack": curses.COLOR_BLACK + 8, "brightred": curses.COLOR_RED + 8, "brightgreen": curses.COLOR_GREEN + 8, "brightyellow": curses.COLOR_YELLOW + 8, "brightblue": curses.COLOR_BLUE + 8, "brightmagenta":curses.COLOR_MAGENTA + 8, "brightcyan": curses.COLOR_CYAN + 8, "brightwhite": curses.COLOR_WHITE + 8, # Aliases "purple": curses.COLOR_MAGENTA, "brightpurple": curses.COLOR_MAGENTA + 8, } def _rgb_to_6cube(r, g, b): # Take an 888 RGB color value and return a 3 tuple representing the index # in the xterm 6x6x6 color cube # Xterm uses a RGB color palette, where the values of each component # can be between 0-5 (therefore forming a 6x6x6 cube). The catch is # that the mapping between RGB and the 6x6x6 cube is non-linear. The # 6x6x6 cube index of 0 is mapped to a RGB value of 0. 1-5 are mapped # to RGB values of 95 with increments of 40. # Sources: # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg # https://github.com/tmux/tmux/blob/master/colour.c # The formula for converting a single RGB value to the 6cube index # 48 is the middle ground between 0 and 95 rgb_to_c6 = lambda x: 0 if x < 48 else max(1, int(round((x - 55) / 40))) return rgb_to_c6(r), rgb_to_c6(g), rgb_to_c6(b) def _rgb_from_6cube(r6, g6, b6): # Take a 666 xterm color cube index and convert it to a # 3 tuple representing the RGB color # The formula from converting the xterm 6cube index to a RGB value rgb_from_c6 = lambda x: 0 if x == 0 else 55 + x*40 return rgb_from_c6(r6), rgb_from_c6(g6), rgb_from_c6(b6) def _rgb_to_gray_idx(r, g, b): # Convert an 888 RGB color to the index of an xterm 256-color grayscale color # with approx. the same perceived brightness. This "grayscale candidate" can # be compared against the best "color candidate" to find a good xterm color # to represent the color. # Calculate the luminance (gray intensity) of a color from its R, G, B components # Source: https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color luma = 0.299 * r + 0.587 * g + 0.114 * b # Closests index in the grayscale palette, which starts at RGB 0x080808, with # stepping 0x0A0A0A idx = int(round((luma - 0x08) / 0x0A)) # Clamp the index to 0-23, corresponding to 232-255 return max(0, min(idx, 23)) def _rgb_from_gray_idx(c): # Convert a grayscale index to its closet single RGB component g = c * 10 + 8 return (g, g, g) # Obscure Python: rgb2index is initialized from a reference to a global {} # Modification to this dictionary are retained between calls _rgb_alloc(), thus # making rgb2index a static variable def _rgb_alloc(rgb, rgb2index={}): # Initialize a new entry in the xterm palette to the given RGB color, returning its # index. If the color has already been initialized, the index of the existing entry is # returned. # Ncurses doesn't allow you to define new colors -- you are allowed only # to overwrite existing ones. # The colors from 0-15 are user-defined and there's no way to query the # RGB values so we better leave them untouched. # The RGB values of colors from 16-255 can be easily calculated. However, # colors from 232-255 use a different formula (grayscale) so stick with # colors from 16-231. 200+ colors should be plentiful. if rgb in rgb2index: return rgb2index[rgb] # The first 16 colors are user defined and we should not change their # values cn = len(rgb2index) + 16 if cn >= curses.COLORS: _warn("Unable to allocate new RGB color.", rgb) return 0 # Map each RGB component from the range 0-255 to the range 0-1000, which # is what curses uses curses.init_color(cn, *(int(round(x * 1000 / 255)) for x in rgb)) rgb2index[rgb] = cn return cn def _color_get(num): # Returns the index of a color that looks like color 'num' in the xterm # 256-color palette. # If ncurses supports color changes, We can't return 'num' directly when # redefining colors, since we might have overwritten the palette entry at # 'num'. Instead, we allocate a new color for it, emulating the 256-color # palette. # Simplest case -- terminal doesn't support changing the definition of # colors. _color_get_rgb() won't be changing the current palettte # so we can return the color as-is if not curses.can_change_color(): return num # Standard colors, _rgb_alloc() doesn't touch these so # we can return them as-is if num < 16 or num >= 232: return num # We're in RGB mode. _rgb_alloc() will redefine the colors # from 16-231. Since the RGB value of the "standard" 256 color # palette can be easily calculated, we can emulate them in RGB # mode rather easily num -= 16 return _rgb_alloc(_rgb_from_6cube((num // 36) % 6, (num // 6) % 6, num % 6)) def _color_get_rgb(rgb): # Lambda for calculating the Euclidean distance between two RGB colors dist = lambda r1, r2: sum((x - y)**2 for x, y in zip(r1, r2)) # Best case -- terminal supports the changing of colors if curses.COLORS >= 256 and curses.can_change_color(): return _rgb_alloc(rgb) # Second best case -- terminal supports 256 colors # Find the closes matching color in the standard 6x6x6 color palette and # the greyscale palette (232-255), compare the two and select the closest # matching color elif curses.COLORS >= 256: # Calculate the indexes of the closest RGB color in the color palette c6 = _rgb_to_6cube(*rgb) # Calculate the RGB value of the closest color crgb = _rgb_from_6cube(*c6) # Calculate the index value of the closest gray palette color gr = _rgb_to_gray_idx(*rgb) # Calculate back RGB value cgr = _rgb_from_gray_idx(gr) if dist(rgb, crgb) < dist(rgb, cgr): # Use the 6x6x6 color palette, calculate the color number # from the 6cube index triplet return 16 + (c6[0] * 36) + (c6[1] * 6) + c6[2] else: # Use the gray palette return 232 + gr # No support for color changes or 256 color mode, this is probably the best # we can do, or is it? Submit patches :) color = 0 dmin = 255**2 + 255**2 + 255**2 for x in range(0, curses.COLORS): crgb = curses.color_content(x) # ncurses returns colors with a range from 0..1000, scale that down # to 0..255 crgb = [int(x * 255 / 1000) for x in crgb] d = dist(rgb, crgb) if d < dmin: dmin = d color = x return color # Dictionary mapping element types to the curses attributes used to display # them _style = {} def _parse_style(style_str, parsing_default): # Parses a string with '=