summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitja HORVAT <pinkfluid@gmail.com>2018-08-29 22:53:49 +0200
committerUlf Magnusson <ulfalizer@gmail.com>2018-09-07 21:11:43 +0200
commitac8d152ec81a82ebacb0e77c3aa6f0cd81638f80 (patch)
tree42fd71790af2184eb0829a1894ab390f6983ef25
parent2be02fac785272e3bcea2cef619954535dc9abe0 (diff)
menuconfig: Add support for custom color schemes (aka styles)
- introduce the _parse_styles() function which can take a string defintion and parse it into a color structure that can be used by UI elements - the _parse_styles() function modifies a global dictionary (_style) which is subsequentially used to style the UI elements - the _style_to_curses() function takes a string representation of style colors and converts it to ncurses attributes. For eaxmple: fg:brightred,bg:black,bold -> returns a color pair representing brightred and black ORed with A_BOLD - simplify _init_styles() as now the majority of the heavy lifting is done by _parse_styles(). _init_styles() initializes the default theme (either "defualt" or "monochrome") - terminals without color capabilities are now handled by using the "monochrome" theme. This theme doesn't use any colors but rather just bold and standout attribtes. - add support for the "underline" attribute - add doctstring at the beginning describing the style syntax - rename the _style() function to _style_attr() as it was conflicting with the global _style dictionary - introduce the StyleError exception; this one is thrown if an error in the style syntax is encountered
-rwxr-xr-xmenuconfig.py365
1 files changed, 245 insertions, 120 deletions
diff --git a/menuconfig.py b/menuconfig.py
index dfc4c90..7c05808 100755
--- a/menuconfig.py
+++ b/menuconfig.py
@@ -48,12 +48,68 @@ $srctree is supported through Kconfiglib.
Color schemes
=============
-Setting the environment variable MENUCONFIG_THEME to 'aquatic' will enable an
+It is possible to customize the color scheme by setting the MENUCONFIG_STYLE
+environment variable. For example, setting it to 'aquatic' will enable an
alternative, less yellow, more 'make menuconfig'-like color scheme, contributed
by Mitja Horvat (pinkfluid).
-See the _init_styles() function if you want to add additional themes. I'm happy
-to take them in upstream.
+This is the current list of built-in styles:
+ - default classic Kconfiglib theme with a yellow accent
+ - monochrome colorless theme (uses only bold and standout) attributes,
+ this style is used if the terminal doesn't support colors
+ - aquatic blue tinted style loosely resembling the lxdialog theme
+
+It is possible to customize the current style by changing colors of UI
+elements on the screen. This is the list of elements that can be stylized:
+
+ - path Top row in the main display, with the menu path
+ - separator Separator lines between windows. Also used for the top line
+ in the symbol
+ - list List of items, e.g. the main display
+ - selection Style for the selected item
+ - inv-list: Like list, but for invisible items. Used in show-all mode.
+ - inv-selection Like selection, but for invisible items. Used in show-all
+ mode.
+ - help Help text windows at the bottom of various fullscreen
+ dialogs
+ - frame Frame around dialog boxes
+ - body Body of dialog boxes
+ - edit Edit box in pop-up dialogs
+ - jump-edit Edit box in jump-to dialog
+ - text Symbol information text
+
+The color definition is a comma separated list of attributes:
+
+ - fg:COLOR Set the foreground/background colors. COLOR can be one of
+ * or * the basic 16 colors (black, red, green, yellow, blue,
+ - bg:COLOR magenta,cyan, white and brighter versions, for example,
+ brightred). On terminals that support 256 color mode, you
+ can use the colorNN keyword, where NN is a number between 1
+ and 255.
+
+ Note: On some terminals a bright version of the color implies
+ bold.
+ - bold Use bold text
+ - underline Use underline text
+ - standout Standout text attribute (reverse color)
+
+A keyword without the '=' is assumed to be a style template. The template name
+is looked up in the built-in styles list and the style definition is expanded
+in-place. With this, built-in styles can be used as basis for new styles.
+
+For example, take the aquatic theme and give it a red selection bar:
+
+MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red"
+
+If there's an error in the style definition, a StyleError exception will be
+thrown briefly describing the error.
+
+The 'default' theme is always implicitly parsed first (or the 'monochrome'
+theme if the terminal lacks colors), so the following two settings have the
+same effect:
+
+ MENUCONFIG_STYLE="selection=fg:white,bg:red"
+ MENUCONFIG_STYLE="default selection=fg:white,bg:red"
Other features
@@ -165,101 +221,172 @@ strings/regexes to find entries that match all of them. Type Ctrl-F to
view the help of the selected item without leaving the dialog.
"""[1:-1].split("\n")
-def _init_styles():
- global _PATH_STYLE
- global _SEPARATOR_STYLE
- global _LIST_STYLE
- global _LIST_SEL_STYLE
- global _LIST_INVISIBLE_STYLE
- global _LIST_INVISIBLE_SEL_STYLE
- global _HELP_STYLE
- global _DIALOG_FRAME_STYLE
- global _DIALOG_BODY_STYLE
- global _DIALOG_EDIT_STYLE
- global _JUMP_TO_EDIT_STYLE
- global _INFO_TEXT_STYLE
-
- # Initialize styles for different parts of the application. The arguments
- # are ordered as follows:
- #
- # 1. Text color
- # 2. Background color
- # 3. Attributes
- # 4. Extra attributes if colors aren't available. The colors will be
- # ignored in this case, and the attributes from (3.) and (4.) will be
- # ORed together.
-
- # A_BOLD tends to produce faint and hard-to-read text on the Windows
- # console, especially with the old color scheme, before the introduction of
- # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/
- BOLD = curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD
-
- # Default styling. Themes can override these settings below.
-
- # Top row in the main display, with the menu path
- PATH_STYLE = (curses.COLOR_BLACK, curses.COLOR_WHITE, BOLD )
-
- # Separator lines between windows. Also used for the top line in the symbol
- # information dialog.
- SEPARATOR_STYLE = (curses.COLOR_BLACK, curses.COLOR_YELLOW, BOLD, curses.A_STANDOUT)
-
- # List of items, e.g. the main display
- LIST_STYLE = (curses.COLOR_BLACK, curses.COLOR_WHITE, curses.A_NORMAL )
-
- # Style for the selected item
- LIST_SEL_STYLE = (curses.COLOR_WHITE, curses.COLOR_BLUE, BOLD, curses.A_STANDOUT)
-
- # Like _LIST_(SEL_)STYLE, for invisible items. Used in show-all mode.
- LIST_INVISIBLE_STYLE = (curses.COLOR_RED, curses.COLOR_WHITE, curses.A_NORMAL, BOLD )
- LIST_INVISIBLE_SEL_STYLE = (curses.COLOR_RED, curses.COLOR_BLUE, BOLD, curses.A_STANDOUT)
-
- # Help text windows at the bottom of various fullscreen dialogs
- HELP_STYLE = PATH_STYLE
-
- # Frame around dialog boxes
- DIALOG_FRAME_STYLE = SEPARATOR_STYLE
+_STYLES = {
+ "default": """
+ path=fg:black,bg:white,bold
+ separator=fg:black,bg:yellow,bold
+ list=fg:black,bg:white
+ selection=fg:white,bg:blue,bold
+ inv-list=fg:red,bg:white
+ inv-selection=fg:red,bg:blue
+ help=fg:black,bg:white,bold
+ frame=fg:black,bg:yellow,bold
+ body=fg:white,bg:black
+ edit=fg:white,bg:blue
+ jump-edit=fg:white,bg:blue
+ text=fg:black,bg:white
+ """,
+
+ # This style is forced on terminals that do no support colors
+ "monochrome": """
+ path=bold
+ separator=bold,standout
+ list=
+ selection=bold,standout
+ inv-list=bold
+ inv-selection=bold,standout
+ help=bold
+ frame=bold,standout
+ body=
+ edit=standout
+ jump-edit=
+ text=
+ """,
+
+ # Blue tinted style loosely resembling lxdialog
+ "aquatic": """
+ path=fg:cyan,bg:blue,bold
+ separator=fg:white,bg:cyan,bold
+ help=fg:cyan,bg:blue,bold
+ frame=fg:white,bg:cyan,bold
+ body=fg:brightwhite,bg:blue
+ edit=fg:black,bg:white
+ """
+}
+
+# Standard colors definition
+_STYLE_STD_COLORS = {
+ # Basic colors
+ "black": curses.COLOR_BLACK,
+ "red": curses.COLOR_RED,
+ "green": curses.COLOR_GREEN,
+ "yellow": curses.COLOR_YELLOW,
+ "blue": curses.COLOR_BLUE,
+ "magenta": curses.COLOR_MAGENTA,
+ "cyan": curses.COLOR_CYAN,
+ "white": curses.COLOR_WHITE,
+
+ # Bright versions
+ "brightblack": curses.COLOR_BLACK + 8,
+ "brightred": curses.COLOR_RED + 8,
+ "brightgreen": curses.COLOR_GREEN + 8,
+ "brightyellow": curses.COLOR_YELLOW + 8,
+ "brightblue": curses.COLOR_BLUE + 8,
+ "brightmagenta":curses.COLOR_MAGENTA + 8,
+ "brightcyan": curses.COLOR_CYAN + 8,
+ "brightwhite": curses.COLOR_WHITE + 8,
+
+ # Aliases
+ "purple": curses.COLOR_MAGENTA,
+ "brightpurple": curses.COLOR_MAGENTA + 8,
+ "default": -1
+}
+
+_style = {}
+
+class StyleError(Exception):
+ pass
+
+def _parse_style(style_str):
+ global _style
+
+ for sline in style_str.split():
+ # Words without a "=" character represents a style template
+ if "=" in sline:
+ key, data = sline.split("=")
+ _style[key] = _style_to_curses(data)
+ else:
+ # Recursively expand style templates
+ if sline not in _STYLES:
+ raise StyleError("Unknown built-in style: {}".format(sline))
- # Body of dialog boxes
- DIALOG_BODY_STYLE = (curses.COLOR_WHITE, curses.COLOR_BLACK, curses.A_NORMAL )
+ # Merge in the template
+ _parse_style(_STYLES[sline])
- # Edit box in pop-up dialogs
- DIALOG_EDIT_STYLE = (curses.COLOR_WHITE, curses.COLOR_BLUE, curses.A_NORMAL, curses.A_STANDOUT)
+def _style_to_curses(cstr):
+ """
+ Parse a style definition and convert it to curses attributes
- # Edit box in jump-to dialog
- JUMP_TO_EDIT_STYLE = (curses.COLOR_WHITE, curses.COLOR_BLUE, curses.A_NORMAL, )
+ This function returns a list of: (fg_color, bg_color, attributes)
+ """
+ def parse_color(t):
+ ts = t.split(":")
+ if len(ts) != 2:
+ raise StyleError("Invalid color notation: {}".format(t))
+
+ cdef = ts[1]
+
+ if cdef in _STYLE_STD_COLORS:
+ return _STYLE_STD_COLORS[cdef]
+
+ if cdef.startswith("color"):
+ cnum = cdef[len("color"):]
+ if not cnum.isdigit():
+ raise StyleError(
+ "Fixed color must be followed by a number. " \
+ "Error near: {}".format(t))
+
+ cnum = int(cnum)
+ if not -1 <= cnum <= curses.COLORS:
+ raise StyleError(
+ "Fixed color must be in range 0..{}. " \
+ "Error near: {}".format(curses.COLORS - 1, t))
+ return cnum
+
+ raise StyleError("Unknown color definition: {}".format(t))
+
+ attrs = 0
+ fg_color = -1
+ bg_color = -1
+
+ if not cstr:
+ return _style_attr(fg_color, bg_color, attrs)
+
+ # Parse attributes
+ for t in cstr.split(","):
+ if t == "bold":
+ # A_BOLD tends to produce faint and hard-to-read text on the Windows
+ # console, especially with the old color scheme, before the
+ # introduction of
+ # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/
+ attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD
+ elif t == "standout":
+ attrs |= curses.A_STANDOUT
+ elif t == "underline":
+ attrs |= curses.A_UNDERLINE
+ elif t.startswith("fg:"):
+ fg_color = parse_color(t)
+ elif t.startswith("bg:"):
+ bg_color = parse_color(t)
+ else:
+ raise StyleError("Invalid style attribute: {}".format(cstr))
- # Symbol information text
- INFO_TEXT_STYLE = LIST_STYLE
+ return _style_attr(fg_color, bg_color, attrs)
- if os.environ.get("MENUCONFIG_THEME") == "aquatic":
- # More 'make menuconfig'-like theme, contributed by Mitja Horvat
- # (pinkfluid)
- PATH_STYLE = (curses.COLOR_CYAN, curses.COLOR_BLUE, BOLD )
- SEPARATOR_STYLE = (curses.COLOR_WHITE, curses.COLOR_CYAN, BOLD, curses.A_STANDOUT)
- HELP_STYLE = PATH_STYLE
- DIALOG_FRAME_STYLE = SEPARATOR_STYLE
- DIALOG_BODY_STYLE = (curses.COLOR_WHITE, curses.COLOR_BLUE, curses.A_NORMAL )
- DIALOG_EDIT_STYLE = (curses.COLOR_BLACK, curses.COLOR_WHITE, curses.A_NORMAL, curses.A_STANDOUT)
+def _init_styles():
+ if curses.has_colors():
+ curses.use_default_colors()
- # Turn styles into attributes and store them in global variables. Doing
- # this separately minimizes the number of curses color pairs, and shortens
- # the style definitions a bit.
- #
- # Could do some locals()/globals() trickery here too, but keep it
- # searchable.
- _PATH_STYLE = _style(*PATH_STYLE)
- _SEPARATOR_STYLE = _style(*SEPARATOR_STYLE)
- _LIST_STYLE = _style(*LIST_STYLE)
- _LIST_SEL_STYLE = _style(*LIST_SEL_STYLE)
- _LIST_INVISIBLE_STYLE = _style(*LIST_INVISIBLE_STYLE)
- _LIST_INVISIBLE_SEL_STYLE = _style(*LIST_INVISIBLE_SEL_STYLE)
- _HELP_STYLE = _style(*HELP_STYLE)
- _DIALOG_FRAME_STYLE = _style(*DIALOG_FRAME_STYLE)
- _DIALOG_BODY_STYLE = _style(*DIALOG_BODY_STYLE)
- _DIALOG_EDIT_STYLE = _style(*DIALOG_EDIT_STYLE)
- _JUMP_TO_EDIT_STYLE = _style(*JUMP_TO_EDIT_STYLE)
- _INFO_TEXT_STYLE = _style(*INFO_TEXT_STYLE)
+ # Force the monochrome style
+ if not curses.has_colors():
+ _parse_style("monochrome")
+ # Use the default style
+ else:
+ _parse_style("default")
+ # Use the user-defined style from the environemnt
+ if "MENUCONFIG_STYLE" in os.environ:
+ _parse_style(os.environ["MENUCONFIG_STYLE"])
#
# Main application
@@ -270,16 +397,15 @@ def _init_styles():
#
# Obscure Python: We never pass a value for color_attribs, and it keeps
# pointing to the same dict. This avoids a global.
-def _style(fg_color, bg_color, attribs, no_color_extra_attribs=0,
- color_attribs={}):
+def _style_attr(fg_color, bg_color, attribs, color_attribs={}):
# Returns an attribute with the specified foreground and background color
# and the attributes in 'attribs'. Reuses color pairs already created if
# possible, and creates a new color pair otherwise.
#
- # Returns 'attribs | no_color_extra_attribs' if colors aren't supported.
+ # Returns 'attribs' if colors aren't supported.
if not curses.has_colors():
- return attribs | no_color_extra_attribs
+ return attribs
if (fg_color, bg_color) not in color_attribs:
# Create new color pair. Color pair number 0 is hardcoded and cannot be
@@ -334,7 +460,6 @@ def menuconfig(kconf):
_kconf = kconf
-
_config_filename = standard_config_filename()
if os.path.exists(_config_filename):
@@ -623,20 +748,20 @@ def _init():
# Initialize windows
# Top row, with menu path
- _path_win = _styled_win(_PATH_STYLE)
+ _path_win = _styled_win(_style["path"])
# Separator below menu path, with title and arrows pointing up
- _top_sep_win = _styled_win(_SEPARATOR_STYLE)
+ _top_sep_win = _styled_win(_style["separator"])
# List of menu entries with symbols, etc.
- _menu_win = _styled_win(_LIST_STYLE)
+ _menu_win = _styled_win(_style["list"])
_menu_win.keypad(True)
# Row below menu list, with arrows pointing down
- _bot_sep_win = _styled_win(_SEPARATOR_STYLE)
+ _bot_sep_win = _styled_win(_style["separator"])
# Help window with keys at the bottom
- _help_win = _styled_win(_HELP_STYLE)
+ _help_win = _styled_win(_style["help"])
# The rows we'd like the nodes in the parent menus to appear on. This
# prevents the scroll from jumping around when going in and out of menus.
@@ -974,10 +1099,10 @@ def _draw_main():
# symbols show up outside show-all mode if an invisible symbol has
# visible children in an implicit (indented) menu.
if not _show_all or (node.prompt and expr_value(node.prompt[1])):
- style = _LIST_SEL_STYLE if i == _sel_node_i else _LIST_STYLE
+ style = _style["selection"] if i == _sel_node_i else _style["list"]
else:
- style = _LIST_INVISIBLE_SEL_STYLE if i == _sel_node_i else \
- _LIST_INVISIBLE_STYLE
+ style = _style["inv-selection"] if i == _sel_node_i else \
+ _style["inv-list"]
_safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style)
@@ -1203,7 +1328,7 @@ def _input_dialog(title, initial_text, info_text=None):
# String to show next to the input field. If None, just the input field
# is shown.
- win = _styled_win(_DIALOG_BODY_STYLE)
+ win = _styled_win(_style["body"])
win.keypad(True)
info_lines = info_text.split("\n") if info_text else []
@@ -1282,7 +1407,7 @@ def _draw_input_dialog(win, title, info_lines, s, i, hscroll):
# Note: Perhaps having a separate window for the input field would be nicer
visible_s = s[hscroll:hscroll + edit_width]
_safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)),
- _DIALOG_EDIT_STYLE)
+ _style["edit"])
for linenr, line in enumerate(info_lines):
_safe_addstr(win, 4 + linenr, 2, line)
@@ -1416,7 +1541,7 @@ def _key_dialog(title, text, keys):
# converted to lowercase. ESC will always close the dialog, and returns
# None.
- win = _styled_win(_DIALOG_BODY_STYLE)
+ win = _styled_win(_style["body"])
win.keypad(True)
_resize_key_dialog(win, text)
@@ -1472,7 +1597,7 @@ def _draw_frame(win, title):
win_height, win_width = win.getmaxyx()
- win.attron(_DIALOG_FRAME_STYLE)
+ win.attron(_style["frame"])
# Draw top/bottom edge
_safe_hline(win, 0, 0, " ", win_width)
@@ -1485,7 +1610,7 @@ def _draw_frame(win, title):
# Draw title
_safe_addstr(win, 0, (win_width - len(title))//2, title)
- win.attroff(_DIALOG_FRAME_STYLE)
+ win.attroff(_style["frame"])
def _jump_to_dialog():
# Implements the jump-to dialog, where symbols can be looked up via
@@ -1509,17 +1634,17 @@ def _jump_to_dialog():
scroll = 0
# Edit box at the top
- edit_box = _styled_win(_JUMP_TO_EDIT_STYLE)
+ edit_box = _styled_win(_style["jump-edit"])
edit_box.keypad(True)
# List of matches
- matches_win = _styled_win(_LIST_STYLE)
+ matches_win = _styled_win(_style["list"])
# Bottom separator, with arrows pointing down
- bot_sep_win = _styled_win(_SEPARATOR_STYLE)
+ bot_sep_win = _styled_win(_style["separator"])
# Help window with instructions at the bottom
- help_win = _styled_win(_HELP_STYLE)
+ help_win = _styled_win(_style["help"])
# Give windows their initial size
_resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
@@ -1735,7 +1860,7 @@ def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
sym_str += ' "{}"'.format(matches[i].prompt[0])
_safe_addstr(matches_win, i - scroll, 0, sym_str,
- _LIST_SEL_STYLE if i == sel_node_i else _LIST_STYLE)
+ _style["selection"] if i == sel_node_i else _style["list"])
else:
# bad_re holds the error message from the re.error exception on errors
@@ -1780,9 +1905,9 @@ def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win,
# Draw arrows pointing up if the symbol list is scrolled down
if scroll > 0:
- # TODO: Bit ugly that _DIALOG_FRAME_STYLE is repeated here
+ # TODO: Bit ugly that _style["frame"] is repeated here
_safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS,
- _DIALOG_FRAME_STYLE)
+ _style["frame"])
visible_s = s[hscroll:hscroll + edit_width]
_safe_addstr(edit_box, 1, 1, visible_s)
@@ -1800,17 +1925,17 @@ def _info_dialog(node, from_jump_to_dialog):
# of the jump-to-dialog.
# Top row, with title and arrows point up
- top_line_win = _styled_win(_SEPARATOR_STYLE)
+ top_line_win = _styled_win(_style["separator"])
# Text display
- text_win = _styled_win(_INFO_TEXT_STYLE)
+ text_win = _styled_win(_style["text"])
text_win.keypad(True)
# Bottom separator, with arrows pointing down
- bot_sep_win = _styled_win(_SEPARATOR_STYLE)
+ bot_sep_win = _styled_win(_style["separator"])
# Help window with keys at the bottom
- help_win = _styled_win(_HELP_STYLE)
+ help_win = _styled_win(_style["help"])
# Give windows their initial size
_resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win)