home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / cli / configPanel.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  23.8 KB  |  612 lines

  1. """
  2. Panel presenting the configuration state for tor or arm. Options can be edited
  3. and the resulting configuration files saved.
  4. """
  5.  
  6. import curses
  7. import threading
  8.  
  9. import cli.controller
  10. import popups
  11.  
  12. from util import conf, enum, panel, sysTools, torConfig, torTools, uiTools
  13.  
  14. DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
  15.                   "features.config.prepopulateEditValues": True,
  16.                   "features.config.state.showPrivateOptions": False,
  17.                   "features.config.state.showVirtualOptions": False,
  18.                   "features.config.state.colWidth.option": 25,
  19.                   "features.config.state.colWidth.value": 15}
  20.  
  21. # TODO: The arm use cases are incomplete since they currently can't be
  22. # modified, have their descriptions fetched, or even get a complete listing
  23. # of what's available.
  24. State = enum.Enum("TOR", "ARM") # state to be presented
  25.  
  26. # mappings of option categories to the color for their entries
  27. CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
  28.                   torConfig.Category.CLIENT: "blue",
  29.                   torConfig.Category.RELAY: "yellow",
  30.                   torConfig.Category.DIRECTORY: "magenta",
  31.                   torConfig.Category.AUTHORITY: "red",
  32.                   torConfig.Category.HIDDEN_SERVICE: "cyan",
  33.                   torConfig.Category.TESTING: "white",
  34.                   torConfig.Category.UNKNOWN: "white"}
  35.  
  36. # attributes of a ConfigEntry
  37. Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
  38.                   "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
  39. DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT)
  40. FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
  41.               Field.OPTION: ("Option Name", "blue"),
  42.               Field.VALUE: ("Value", "cyan"),
  43.               Field.TYPE: ("Arg Type", "green"),
  44.               Field.ARG_USAGE: ("Arg Usage", "yellow"),
  45.               Field.SUMMARY: ("Summary", "green"),
  46.               Field.DESCRIPTION: ("Description", "white"),
  47.               Field.MAN_ENTRY: ("Man Page Entry", "blue"),
  48.               Field.IS_DEFAULT: ("Is Default", "magenta")}
  49.  
  50. def getFieldFromLabel(fieldLabel):
  51.   """
  52.   Converts field labels back to their enumeration, raising a ValueError if it
  53.   doesn't exist.
  54.   """
  55.   
  56.   for entryEnum in FIELD_ATTR:
  57.     if fieldLabel == FIELD_ATTR[entryEnum][0]:
  58.       return entryEnum
  59.  
  60. class ConfigEntry():
  61.   """
  62.   Configuration option in the panel.
  63.   """
  64.   
  65.   def __init__(self, option, type, isDefault):
  66.     self.fields = {}
  67.     self.fields[Field.OPTION] = option
  68.     self.fields[Field.TYPE] = type
  69.     self.fields[Field.IS_DEFAULT] = isDefault
  70.     
  71.     # Fetches extra infromation from external sources (the arm config and tor
  72.     # man page). These are None if unavailable for this config option.
  73.     summary = torConfig.getConfigSummary(option)
  74.     manEntry = torConfig.getConfigDescription(option)
  75.     
  76.     if manEntry:
  77.       self.fields[Field.MAN_ENTRY] = manEntry.index
  78.       self.fields[Field.CATEGORY] = manEntry.category
  79.       self.fields[Field.ARG_USAGE] = manEntry.argUsage
  80.       self.fields[Field.DESCRIPTION] = manEntry.description
  81.     else:
  82.       self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
  83.       self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN
  84.       self.fields[Field.ARG_USAGE] = ""
  85.       self.fields[Field.DESCRIPTION] = ""
  86.     
  87.     # uses the full man page description if a summary is unavailable
  88.     self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION]
  89.     
  90.     # cache of what's displayed for this configuration option
  91.     self.labelCache = None
  92.     self.labelCacheArgs = None
  93.   
  94.   def get(self, field):
  95.     """
  96.     Provides back the value in the given field.
  97.     
  98.     Arguments:
  99.       field - enum for the field to be provided back
  100.     """
  101.     
  102.     if field == Field.VALUE: return self._getValue()
  103.     else: return self.fields[field]
  104.   
  105.   def getAll(self, fields):
  106.     """
  107.     Provides back a list with the given field values.
  108.     
  109.     Arguments:
  110.       field - enums for the fields to be provided back
  111.     """
  112.     
  113.     return [self.get(field) for field in fields]
  114.   
  115.   def getLabel(self, optionWidth, valueWidth, summaryWidth):
  116.     """
  117.     Provides display string of the configuration entry with the given
  118.     constraints on the width of the contents.
  119.     
  120.     Arguments:
  121.       optionWidth  - width of the option column
  122.       valueWidth   - width of the value column
  123.       summaryWidth - width of the summary column
  124.     """
  125.     
  126.     # Fetching the display entries is very common so this caches the values.
  127.     # Doing this substantially drops cpu usage when scrolling (by around 40%).
  128.     
  129.     argSet = (optionWidth, valueWidth, summaryWidth)
  130.     if not self.labelCache or self.labelCacheArgs != argSet:
  131.       optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth)
  132.       valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
  133.       summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
  134.       lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
  135.       self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
  136.       self.labelCacheArgs = argSet
  137.     
  138.     return self.labelCache
  139.   
  140.   def isUnset(self):
  141.     """
  142.     True if we have no value, false otherwise.
  143.     """
  144.     
  145.     confValue = torTools.getConn().getOption(self.get(Field.OPTION), [], True)
  146.     return not bool(confValue)
  147.   
  148.   def _getValue(self):
  149.     """
  150.     Provides the current value of the configuration entry, taking advantage of
  151.     the torTools caching to effectively query the accurate value. This uses the
  152.     value's type to provide a user friendly representation if able.
  153.     """
  154.     
  155.     confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True))
  156.     
  157.     # provides nicer values for recognized types
  158.     if not confValue: confValue = "<none>"
  159.     elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"):
  160.       confValue = "False" if confValue == "0" else "True"
  161.     elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
  162.       confValue = uiTools.getSizeLabel(int(confValue))
  163.     elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
  164.       confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
  165.     
  166.     return confValue
  167.  
  168. class ConfigPanel(panel.Panel):
  169.   """
  170.   Renders a listing of the tor or arm configuration state, allowing options to
  171.   be selected and edited.
  172.   """
  173.   
  174.   def __init__(self, stdscr, configType, config=None):
  175.     panel.Panel.__init__(self, stdscr, "configuration", 0)
  176.     
  177.     self.sortOrdering = DEFAULT_SORT_ORDER
  178.     self._config = dict(DEFAULT_CONFIG)
  179.     if config:
  180.       config.update(self._config, {
  181.         "features.config.selectionDetails.height": 0,
  182.         "features.config.state.colWidth.option": 5,
  183.         "features.config.state.colWidth.value": 5})
  184.       
  185.       sortFields = Field.values()
  186.       customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
  187.       
  188.       if customOrdering:
  189.         self.sortOrdering = [sortFields[i] for i in customOrdering]
  190.     
  191.     self.configType = configType
  192.     self.confContents = []
  193.     self.confImportantContents = []
  194.     self.scroller = uiTools.Scroller(True)
  195.     self.valsLock = threading.RLock()
  196.     
  197.     # shows all configuration options if true, otherwise only the ones with
  198.     # the 'important' flag are shown
  199.     self.showAll = False
  200.     
  201.     # initializes config contents if we're connected
  202.     conn = torTools.getConn()
  203.     conn.addStatusListener(self.resetListener)
  204.     if conn.isAlive(): self.resetListener(conn, torTools.State.INIT)
  205.   
  206.   def resetListener(self, conn, eventType):
  207.     # fetches configuration options if a new instance, otherewise keeps our
  208.     # current contents
  209.     
  210.     if eventType == torTools.State.INIT:
  211.       self._loadConfigOptions()
  212.   
  213.   def _loadConfigOptions(self):
  214.     """
  215.     Fetches the configuration options available from tor or arm.
  216.     """
  217.     
  218.     self.confContents = []
  219.     self.confImportantContents = []
  220.     
  221.     if self.configType == State.TOR:
  222.       conn, configOptionLines = torTools.getConn(), []
  223.       customOptions = torConfig.getCustomOptions()
  224.       configOptionQuery = conn.getInfo("config/names")
  225.       
  226.       if configOptionQuery:
  227.         configOptionLines = configOptionQuery.strip().split("\n")
  228.       
  229.       for line in configOptionLines:
  230.         # lines are of the form "<option> <type>[ <documentation>]", like:
  231.         # UseEntryGuards Boolean
  232.         # documentation is aparently only in older versions (for instance,
  233.         # 0.2.1.25)
  234.         lineComp = line.strip().split(" ")
  235.         confOption, confType = lineComp[0], lineComp[1]
  236.         
  237.         # skips private and virtual entries if not configured to show them
  238.         if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
  239.           continue
  240.         elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
  241.           continue
  242.         
  243.         self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
  244.     elif self.configType == State.ARM:
  245.       # loaded via the conf utility
  246.       armConf = conf.getConfig("arm")
  247.       for key in armConf.getKeys():
  248.         pass # TODO: implement
  249.     
  250.     # mirror listing with only the important configuration options
  251.     self.confImportantContents = []
  252.     for entry in self.confContents:
  253.       if torConfig.isImportant(entry.get(Field.OPTION)):
  254.         self.confImportantContents.append(entry)
  255.     
  256.     # if there aren't any important options then show everything
  257.     if not self.confImportantContents:
  258.       self.confImportantContents = self.confContents
  259.     
  260.     self.setSortOrder() # initial sorting of the contents
  261.   
  262.   def getSelection(self):
  263.     """
  264.     Provides the currently selected entry.
  265.     """
  266.     
  267.     return self.scroller.getCursorSelection(self._getConfigOptions())
  268.   
  269.   def setFiltering(self, isFiltered):
  270.     """
  271.     Sets if configuration options are filtered or not.
  272.     
  273.     Arguments:
  274.       isFiltered - if true then only relatively important options will be
  275.                    shown, otherwise everything is shown
  276.     """
  277.     
  278.     self.showAll = not isFiltered
  279.   
  280.   def setSortOrder(self, ordering = None):
  281.     """
  282.     Sets the configuration attributes we're sorting by and resorts the
  283.     contents.
  284.     
  285.     Arguments:
  286.       ordering - new ordering, if undefined then this resorts with the last
  287.                  set ordering
  288.     """
  289.     
  290.     self.valsLock.acquire()
  291.     if ordering: self.sortOrdering = ordering
  292.     self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
  293.     self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
  294.     self.valsLock.release()
  295.   
  296.   def showSortDialog(self):
  297.     """
  298.     Provides the sort dialog for our configuration options.
  299.     """
  300.     
  301.     # set ordering for config options
  302.     titleLabel = "Config Option Ordering:"
  303.     options = [FIELD_ATTR[field][0] for field in Field.values()]
  304.     oldSelection = [FIELD_ATTR[field][0] for field in self.sortOrdering]
  305.     optionColors = dict([FIELD_ATTR[field] for field in Field.values()])
  306.     results = popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
  307.     
  308.     if results:
  309.       # converts labels back to enums
  310.       resultEnums = [getFieldFromLabel(label) for label in results]
  311.       self.setSortOrder(resultEnums)
  312.   
  313.   def handleKey(self, key):
  314.     self.valsLock.acquire()
  315.     isKeystrokeConsumed = True
  316.     if uiTools.isScrollKey(key):
  317.       pageHeight = self.getPreferredSize()[0] - 1
  318.       detailPanelHeight = self._config["features.config.selectionDetails.height"]
  319.       if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
  320.         pageHeight -= (detailPanelHeight + 1)
  321.       
  322.       isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight)
  323.       if isChanged: self.redraw(True)
  324.     elif uiTools.isSelectionKey(key) and self._getConfigOptions():
  325.       # Prompts the user to edit the selected configuration value. The
  326.       # interface is locked to prevent updates between setting the value
  327.       # and showing any errors.
  328.       
  329.       panel.CURSES_LOCK.acquire()
  330.       try:
  331.         selection = self.getSelection()
  332.         configOption = selection.get(Field.OPTION)
  333.         if selection.isUnset(): initialValue = ""
  334.         else: initialValue = selection.get(Field.VALUE)
  335.         
  336.         promptMsg = "%s Value (esc to cancel): " % configOption
  337.         isPrepopulated = self._config["features.config.prepopulateEditValues"]
  338.         newValue = popups.inputPrompt(promptMsg, initialValue if isPrepopulated else "")
  339.         
  340.         if newValue != None and newValue != initialValue:
  341.           try:
  342.             if selection.get(Field.TYPE) == "Boolean":
  343.               # if the value's a boolean then allow for 'true' and 'false' inputs
  344.               if newValue.lower() == "true": newValue = "1"
  345.               elif newValue.lower() == "false": newValue = "0"
  346.             elif selection.get(Field.TYPE) == "LineList":
  347.               # setOption accepts list inputs when there's multiple values
  348.               newValue = newValue.split(",")
  349.             
  350.             torTools.getConn().setOption(configOption, newValue)
  351.             
  352.             # forces the label to be remade with the new value
  353.             selection.labelCache = None
  354.             
  355.             # resets the isDefault flag
  356.             customOptions = torConfig.getCustomOptions()
  357.             selection.fields[Field.IS_DEFAULT] = not configOption in customOptions
  358.             
  359.             self.redraw(True)
  360.           except Exception, exc:
  361.             popups.showMsg("%s (press any key)" % exc)
  362.       finally:
  363.         panel.CURSES_LOCK.release()
  364.     elif key == ord('a') or key == ord('A'):
  365.       self.showAll = not self.showAll
  366.       self.redraw(True)
  367.     elif key == ord('s') or key == ord('S'):
  368.       self.showSortDialog()
  369.     elif key == ord('v') or key == ord('V'):
  370.       self.showWriteDialog()
  371.     else: isKeystrokeConsumed = False
  372.     
  373.     self.valsLock.release()
  374.     return isKeystrokeConsumed
  375.   
  376.   def showWriteDialog(self):
  377.     """
  378.     Provies an interface to confirm if the configuration is saved and, if so,
  379.     where.
  380.     """
  381.     
  382.     # display a popup for saving the current configuration
  383.     configLines = torConfig.getCustomOptions(True)
  384.     popup, width, height = popups.init(len(configLines) + 2)
  385.     if not popup: return
  386.     
  387.     try:
  388.       # displayed options (truncating the labels if there's limited room)
  389.       if width >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
  390.       else: selectionOptions = ("Save", "Save As", "X")
  391.       
  392.       # checks if we can show options beside the last line of visible content
  393.       isOptionLineSeparate = False
  394.       lastIndex = min(height - 2, len(configLines) - 1)
  395.       
  396.       # if we don't have room to display the selection options and room to
  397.       # grow then display the selection options on its own line
  398.       if width < (30 + len(configLines[lastIndex])):
  399.         popup.setHeight(height + 1)
  400.         popup.redraw(True) # recreates the window instance
  401.         newHeight, _ = popup.getPreferredSize()
  402.         
  403.         if newHeight > height:
  404.           height = newHeight
  405.           isOptionLineSeparate = True
  406.       
  407.       key, selection = 0, 2
  408.       while not uiTools.isSelectionKey(key):
  409.         # if the popup has been resized then recreate it (needed for the
  410.         # proper border height)
  411.         newHeight, newWidth = popup.getPreferredSize()
  412.         if (height, width) != (newHeight, newWidth):
  413.           height, width = newHeight, newWidth
  414.           popup.redraw(True)
  415.         
  416.         # if there isn't room to display the popup then cancel it
  417.         if height <= 2:
  418.           selection = 2
  419.           break
  420.         
  421.         popup.win.erase()
  422.         popup.win.box()
  423.         popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
  424.         
  425.         visibleConfigLines = height - 3 if isOptionLineSeparate else height - 2
  426.         for i in range(visibleConfigLines):
  427.           line = uiTools.cropStr(configLines[i], width - 2)
  428.           
  429.           if " " in line:
  430.             option, arg = line.split(" ", 1)
  431.             popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
  432.             popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
  433.           else:
  434.             popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
  435.         
  436.         # draws selection options (drawn right to left)
  437.         drawX = width - 1
  438.         for i in range(len(selectionOptions) - 1, -1, -1):
  439.           optionLabel = selectionOptions[i]
  440.           drawX -= (len(optionLabel) + 2)
  441.           
  442.           # if we've run out of room then drop the option (this will only
  443.           # occure on tiny displays)
  444.           if drawX < 1: break
  445.           
  446.           selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
  447.           popup.addstr(height - 2, drawX, "[")
  448.           popup.addstr(height - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
  449.           popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]")
  450.           
  451.           drawX -= 1 # space gap between the options
  452.         
  453.         popup.win.refresh()
  454.         
  455.         key = cli.controller.getController().getScreen().getch()
  456.         if key == curses.KEY_LEFT: selection = max(0, selection - 1)
  457.         elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
  458.       
  459.       if selection in (0, 1):
  460.         loadedTorrc, promptCanceled = torConfig.getTorrc(), False
  461.         try: configLocation = loadedTorrc.getConfigLocation()
  462.         except IOError: configLocation = ""
  463.         
  464.         if selection == 1:
  465.           # prompts user for a configuration location
  466.           configLocation = popups.inputPrompt("Save to (esc to cancel): ", configLocation)
  467.           if not configLocation: promptCanceled = True
  468.         
  469.         if not promptCanceled:
  470.           try:
  471.             torConfig.saveConf(configLocation, configLines)
  472.             msg = "Saved configuration to %s" % configLocation
  473.           except IOError, exc:
  474.             msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
  475.           
  476.           popups.showMsg(msg, 2)
  477.     finally: popups.finalize()
  478.   
  479.   def getHelp(self):
  480.     options = []
  481.     options.append(("up arrow", "scroll up a line", None))
  482.     options.append(("down arrow", "scroll down a line", None))
  483.     options.append(("page up", "scroll up a page", None))
  484.     options.append(("page down", "scroll down a page", None))
  485.     options.append(("enter", "edit configuration option", None))
  486.     options.append(("v", "save configuration", None))
  487.     options.append(("a", "toggle option filtering", None))
  488.     options.append(("s", "sort ordering", None))
  489.     return options
  490.   
  491.   def draw(self, width, height):
  492.     self.valsLock.acquire()
  493.     
  494.     # panel with details for the current selection
  495.     detailPanelHeight = self._config["features.config.selectionDetails.height"]
  496.     isScrollbarVisible = False
  497.     if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
  498.       # no detail panel
  499.       detailPanelHeight = 0
  500.       scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
  501.       cursorSelection = self.getSelection()
  502.       isScrollbarVisible = len(self._getConfigOptions()) > height - 1
  503.     else:
  504.       # Shrink detail panel if there isn't sufficient room for the whole
  505.       # thing. The extra line is for the bottom border.
  506.       detailPanelHeight = min(height - 1, detailPanelHeight + 1)
  507.       scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
  508.       cursorSelection = self.getSelection()
  509.       isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
  510.       
  511.       if cursorSelection != None:
  512.         self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
  513.     
  514.     # draws the top label
  515.     if self.isTitleVisible():
  516.       configType = "Tor" if self.configType == State.TOR else "Arm"
  517.       hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
  518.       titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
  519.       self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
  520.     
  521.     # draws left-hand scroll bar if content's longer than the height
  522.     scrollOffset = 1
  523.     if isScrollbarVisible:
  524.       scrollOffset = 3
  525.       self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
  526.     
  527.     optionWidth = self._config["features.config.state.colWidth.option"]
  528.     valueWidth = self._config["features.config.state.colWidth.value"]
  529.     descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
  530.     
  531.     # if the description column is overly long then use its space for the
  532.     # value instead
  533.     if descriptionWidth > 80:
  534.       valueWidth += descriptionWidth - 80
  535.       descriptionWidth = 80
  536.     
  537.     for lineNum in range(scrollLoc, len(self._getConfigOptions())):
  538.       entry = self._getConfigOptions()[lineNum]
  539.       drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
  540.       
  541.       lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD
  542.       if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)])
  543.       if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
  544.       
  545.       lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth)
  546.       self.addstr(drawLine, scrollOffset, lineText, lineFormat)
  547.       
  548.       if drawLine >= height: break
  549.     
  550.     self.valsLock.release()
  551.   
  552.   def _getConfigOptions(self):
  553.     return self.confContents if self.showAll else self.confImportantContents
  554.   
  555.   def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
  556.     """
  557.     Renders a panel for the selected configuration option.
  558.     """
  559.     
  560.     # This is a solid border unless the scrollbar is visible, in which case a
  561.     # 'T' pipe connects the border to the bar.
  562.     uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
  563.     if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
  564.     
  565.     selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
  566.     
  567.     # first entry:
  568.     # <option> (<category> Option)
  569.     optionLabel =" (%s Option)" % selection.get(Field.CATEGORY)
  570.     self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
  571.     
  572.     # second entry:
  573.     # Value: <value> ([default|custom], <type>, usage: <argument usage>)
  574.     if detailPanelHeight >= 3:
  575.       valueAttr = []
  576.       valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
  577.       valueAttr.append(selection.get(Field.TYPE))
  578.       valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
  579.       valueAttrLabel = ", ".join(valueAttr)
  580.       
  581.       valueLabelWidth = width - 12 - len(valueAttrLabel)
  582.       valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
  583.       
  584.       self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
  585.     
  586.     # remainder is filled with the man page description
  587.     descriptionHeight = max(0, detailPanelHeight - 3)
  588.     descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
  589.     
  590.     for i in range(descriptionHeight):
  591.       # checks if we're done writing the description
  592.       if not descriptionContent: break
  593.       
  594.       # there's a leading indent after the first line
  595.       if i > 0: descriptionContent = "  " + descriptionContent
  596.       
  597.       # we only want to work with content up until the next newline
  598.       if "\n" in descriptionContent:
  599.         lineContent, descriptionContent = descriptionContent.split("\n", 1)
  600.       else: lineContent, descriptionContent = descriptionContent, ""
  601.       
  602.       if i != descriptionHeight - 1:
  603.         # there's more lines to display
  604.         msg, remainder = uiTools.cropStr(lineContent, width - 3, 4, 4, uiTools.Ending.HYPHEN, True)
  605.         descriptionContent = remainder.strip() + descriptionContent
  606.       else:
  607.         # this is the last line, end it with an ellipse
  608.         msg = uiTools.cropStr(lineContent, width - 3, 4, 4)
  609.       
  610.       self.addstr(3 + i, 2, msg, selectionFormat)
  611.  
  612.