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