[tor-commits] [arm/master] Revised color util handling

atagar at torproject.org atagar at torproject.org
Mon Jan 27 02:32:35 UTC 2014


commit 7a15738d01cb62e47e776ed41e4fb0a227f33dbb
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Jan 26 13:24:26 2014 -0800

    Revised color util handling
    
    Cleaning up our ui_tools' color functions.
---
 arm/config/strings.cfg |    7 +-
 arm/controller.py      |    6 +-
 arm/util/tracker.py    |    4 +-
 arm/util/ui_tools.py   |  280 ++++++++++++++++++++++++------------------------
 4 files changed, 151 insertions(+), 146 deletions(-)

diff --git a/arm/config/strings.cfg b/arm/config/strings.cfg
index 60194d2..17a9cd7 100644
--- a/arm/config/strings.cfg
+++ b/arm/config/strings.cfg
@@ -12,6 +12,8 @@
 #
 ################################################################################
 
+msg.wrap {text}
+
 msg.config.unable_to_load_settings Unable to load arm's internal configurations: {error}
 msg.config.unable_to_read_file Failed to load configuration (using defaults): "{error}"
 msg.config.nothing_loaded No armrc loaded, using defaults. You can customize arm by placing a configuration file at {path} (see the armrc.sample for its options).
@@ -34,6 +36,8 @@ msg.setup.set_freebsd_chroot Adjusting paths to account for Tor running in a Fre
 msg.setup.tor_is_running_as_root Tor is currently running with root permissions. This isn't a good idea, nor should it be necessary. See the 'User UID' option on Tor's man page for an easy method of reducing its permissions after startup.
 msg.setup.unable_to_determine_pid Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.
 msg.setup.unknown_event_types arm doesn't recognize the following event types: {event_types} (log 'UNKNOWN' events to see them)
+msg.setup.color_support_available Terminal color support detected and enabled
+msg.setup.color_support_unavailable Terminal color support unavailable
 
 msg.tracker.abort_getting_resources Failed three attempts to get process resource usage from {resolver}, {response} ({exc})
 msg.tracker.abort_getting_port_usage Failed three attempts to determine the process using active ports ({exc})
@@ -45,8 +49,9 @@ msg.tracker.unable_to_use_resolver Unable to query connections with {old_resolve
 
 msg.usage.invalid_arguments {error} (for usage provide --help)
 msg.usage.not_a_valid_address '{address_input}' isn't a valid IPv4 address
-msg.not_a_valid_port '{port_input}' isn't a valid port number
+msg.usage.not_a_valid_port '{port_input}' isn't a valid port number
 msg.usage.unrecognized_log_flags Unrecognized event flags: {flags}
+msg.usage.unable_to_set_color_override "{color}" isn't a valid color
 
 msg.connect.missing_password_bug
 |BUG: You provided a password but despite this stem reported that it was
diff --git a/arm/controller.py b/arm/controller.py
index 98b5adb..e684215 100644
--- a/arm/controller.py
+++ b/arm/controller.py
@@ -24,7 +24,7 @@ import arm.util.tracker
 
 from stem.control import State
 
-from arm.util import panel, tor_config, tor_tools
+from arm.util import panel, tor_config, tor_tools, ui_tools
 
 from stem.util import conf, enum, log, system
 
@@ -41,6 +41,7 @@ def conf_handler(key, value):
 CONFIG = conf.config_dict("arm", {
   "startup.events": "N3",
   "startup.data_directory": "~/.arm",
+  "features.acsSupport": True,
   "features.panels.show.graph": True,
   "features.panels.show.log": True,
   "features.panels.show.connection": True,
@@ -587,6 +588,9 @@ def start_arm(stdscr):
   init_controller(stdscr, start_time)
   control = get_controller()
 
+  if not CONFIG["features.acsSupport"]:
+    ui_tools.disable_acs()
+
   # provides notice about any unused config keys
 
   for key in conf.get_config("arm").unused_keys():
diff --git a/arm/util/tracker.py b/arm/util/tracker.py
index e56630d..a10d291 100644
--- a/arm/util/tracker.py
+++ b/arm/util/tracker.py
@@ -40,7 +40,7 @@ import time
 import threading
 
 from stem.control import State
-from stem.util import conf, connection, log, proc, str_tools, system
+from stem.util import conf, connection, proc, str_tools, system
 
 from arm.util import tor_controller, debug, info, notice
 
@@ -465,7 +465,7 @@ class ConnectionTracker(Daemon):
 
       return True
     except IOError as exc:
-      log.info(exc)
+      info('wrap', text = exc)
 
       # Fail over to another resolver if we've repeatedly been unable to use
       # this one.
diff --git a/arm/util/ui_tools.py b/arm/util/ui_tools.py
index 1d90001..9e4a622 100644
--- a/arm/util/ui_tools.py
+++ b/arm/util/ui_tools.py
@@ -1,8 +1,5 @@
 """
-Toolkit for common ui tasks when working with curses. This provides a quick and
-easy method of providing the following interface components:
-- preinitialized curses color attributes
-- unit conversion for labels
+Toolkit for working with curses.
 """
 
 import sys
@@ -10,88 +7,70 @@ import curses
 
 from curses.ascii import isprint
 
-from stem.util import conf, enum, log, system
+from arm.util import info, msg
 
-# colors curses can handle
+from stem.util import conf, enum, system
 
 COLOR_LIST = {
-  "red": curses.COLOR_RED,
-  "green": curses.COLOR_GREEN,
-  "yellow": curses.COLOR_YELLOW,
-  "blue": curses.COLOR_BLUE,
-  "cyan": curses.COLOR_CYAN,
-  "magenta": curses.COLOR_MAGENTA,
-  "black": curses.COLOR_BLACK,
-  "white": curses.COLOR_WHITE,
+  'red': curses.COLOR_RED,
+  'green': curses.COLOR_GREEN,
+  'yellow': curses.COLOR_YELLOW,
+  'blue': curses.COLOR_BLUE,
+  'cyan': curses.COLOR_CYAN,
+  'magenta': curses.COLOR_MAGENTA,
+  'black': curses.COLOR_BLACK,
+  'white': curses.COLOR_WHITE,
 }
 
-# boolean for if we have color support enabled, None not yet determined
+DEFAULT_COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST])
+COLOR_ATTR = None
 
-COLOR_IS_SUPPORTED = None
-
-# mappings for get_color() - this uses the default terminal color scheme if
-# color support is unavailable
-
-COLOR_ATTR_INITIALIZED = False
-COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST])
-
-Ending = enum.Enum("ELLIPSE", "HYPHEN")
+Ending = enum.Enum('ELLIPSE', 'HYPHEN')
 SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
 
 
 def conf_handler(key, value):
-  if key == "features.color_override" and value != "none":
-    try:
-      set_color_override(value)
-    except ValueError as exc:
-      log.notice(exc)
+  if key == 'features.color_override':
+    if value not in COLOR_LIST.keys() and value != 'none':
+      raise ValueError(msg('usage.unable_to_set_color_override', color = value))
 
 
-CONFIG = conf.config_dict("arm", {
-  "features.color_override": "none",
-  "features.colorInterface": True,
-  "features.acsSupport": True,
+CONFIG = conf.config_dict('arm', {
+  'features.color_override': 'none',
+  'features.colorInterface': True,
 }, conf_handler)
 
 
-def get_printable(line, keep_newlines = True):
-  """
-  Provides the line back with non-printable characters stripped.
-
-  Arguments:
-    line          - string to be processed
-    stripNewlines - retains newlines if true, stripped otherwise
-  """
-
-  line = line.replace('\xc2', "'")
-  line = "".join([char for char in line if (isprint(char) or (keep_newlines and char == "\n"))])
-
-  return line
-
-
 def is_color_supported():
   """
-  True if the display supports showing color, false otherwise.
-  """
+  Checks if curses presently supports rendering colors.
 
-  if COLOR_IS_SUPPORTED is None:
-    _init_colors()
+  :returns: **True** if colors can be rendered, **False** otherwise
+  """
 
-  return COLOR_IS_SUPPORTED
+  return _color_attr() != DEFAULT_COLOR_ATTR
 
 
 def get_color(color):
   """
   Provides attribute corresponding to a given text color. Supported colors
   include:
-  red       green     yellow    blue
-  cyan      magenta   black     white
+
+    * red
+    * green
+    * yellow
+    * blue
+    * cyan
+    * magenta
+    * black
+    * white
 
   If color support isn't available or colors can't be initialized then this uses the
   terminal's default coloring scheme.
 
-  Arguments:
-    color - name of the foreground color to be returned
+  :param str color: color attributes to be provided
+
+  :returns: **tuple** color pair used by curses to render the color
   """
 
   color_override = get_color_override()
@@ -99,43 +78,111 @@ def get_color(color):
   if color_override:
     color = color_override
 
-  if not COLOR_ATTR_INITIALIZED:
-    _init_colors()
-
-  return COLOR_ATTR[color]
+  return _color_attr()[color]
 
 
 def set_color_override(color = None):
   """
-  Overwrites all requests for color with the given color instead. This raises
-  a ValueError if the color is invalid.
+  Overwrites all requests for color with the given color instead.
 
-  Arguments:
-    color - name of the color to overwrite requests with, None to use normal
-            coloring
+  :param str color: color to override all requests with, **None** if color
+    requests shouldn't be overwritten
+
+  :raises: **ValueError** if the color name is invalid
   """
 
+  arm_config = conf.get_config('arm')
+
   if color is None:
-    CONFIG["features.color_override"] = "none"
+    arm_config.set('features.color_override', 'none')
   elif color in COLOR_LIST.keys():
-    CONFIG["features.color_override"] = color
+    arm_config.set('features.color_override', color)
   else:
-    raise ValueError("\"%s\" isn't a valid color" % color)
+    raise ValueError(msg('usage.unable_to_set_color_override', color = color))
 
 
 def get_color_override():
   """
-  Provides the override color used by the interface, None if it isn't set.
+  Provides the override color used by the interface.
+
+  :returns: **str** for the color requrests will be overwritten with, **None**
+    if no override is set
   """
 
-  color_override = CONFIG.get("features.color_override", "none")
+  color_override = CONFIG.get('features.color_override', 'none')
 
-  if color_override == "none":
+  if color_override == 'none':
     return None
   else:
     return color_override
 
 
+def _color_attr():
+  """
+  Initializes color mappings usable by curses. This can only be done after
+  calling curses.initscr().
+  """
+
+  global COLOR_ATTR
+
+  if COLOR_ATTR is None:
+    if not CONFIG['features.colorInterface']:
+      COLOR_ATTR = DEFAULT_COLOR_ATTR
+    elif curses.has_colors():
+      color_attr = dict([(color, 0) for color in COLOR_LIST])
+
+      for color_pair, color_name in enumerate(COLOR_LIST):
+        foreground_color = COLOR_LIST[color_name]
+        background_color = -1  # allows for default (possibly transparent) background
+        curses.init_pair(color_pair + 1, foreground_color, background_color)
+        color_attr[color_name] = curses.color_pair(color_pair + 1)
+
+      info('setup.color_support_available')
+      COLOR_ATTR = color_attr
+    else:
+      info('setup.color_support_unavailable')
+      COLOR_ATTR = DEFAULT_COLOR_ATTR
+
+  return COLOR_ATTR
+
+
+def disable_acs():
+  """
+  Replaces the curses ACS characters. This can be preferable if curses is
+  unable to render them...
+
+  http://www.atagar.com/arm/images/acs_display_failure.png
+  """
+
+  for item in curses.__dict__:
+    if item.startswith('ACS_'):
+      curses.__dict__[item] = ord('+')
+
+  # replace a few common border pipes that are better rendered as '|' or
+  # '-' instead
+
+  curses.ACS_SBSB = ord('|')
+  curses.ACS_VLINE = ord('|')
+  curses.ACS_BSBS = ord('-')
+  curses.ACS_HLINE = ord('-')
+
+
+def get_printable(line, keep_newlines = True):
+  """
+  Provides the line back with non-printable characters stripped.
+
+  :param str line: string to be processed
+  :param str keep_newlines: retains newlines if **True**, stripped otherwise
+
+  :returns: **str** of the line with only printable content
+  """
+
+  line = line.replace('\xc2', "'")
+  line = filter(lambda char: isprint(char) or (keep_newlines and char == '\n'), line)
+
+  return line
+
+
 def crop_str(msg, size, min_word_length = 4, min_crop = 0, end_type = Ending.ELLIPSE, get_remainder = False):
   """
   Provides the msg constrained to the given length, truncating on word breaks.
@@ -143,29 +190,29 @@ def crop_str(msg, size, min_word_length = 4, min_crop = 0, end_type = Ending.ELL
   isn't room for even a truncated single word (or one word plus the ellipse if
   including those) then this provides an empty string. If a cropped string ends
   with a comma or period then it's stripped (unless we're providing the
-  remainder back). Examples:
+  remainder back). For example...
 
-  crop_str("This is a looooong message", 17)
-  "This is a looo..."
+    >>> crop_str("This is a looooong message", 17)
+    "This is a looo..."
 
-  crop_str("This is a looooong message", 12)
-  "This is a..."
+    >>> crop_str("This is a looooong message", 12)
+    "This is a..."
 
-  crop_str("This is a looooong message", 3)
-  ""
+    >>> crop_str("This is a looooong message", 3)
+    ""
 
-  Arguments:
-    msg             - source text
-    size            - room available for text
-    min_word_length - minimum characters before which a word is dropped, requires
-                      whole word if None
-    min_crop        - minimum characters that must be dropped if a word's cropped
-    end_type        - type of ending used when truncating:
-                      None - blank ending
-                      Ending.ELLIPSE - includes an ellipse
-                      Ending.HYPHEN - adds hyphen when breaking words
-    get_remainder   - returns a tuple instead, with the second part being the
-                      cropped portion of the message
+  :param str msg: text to be processed
+  :param int size: space available for text
+  :param int min_word_length: minimum characters before which a word is
+    dropped, requires whole word if **None**
+  :param int min_crop: minimum characters that must be dropped if a word is
+    cropped
+  :param Ending end_type: type of ending used when truncating, no special
+    truncation is used if **None**
+  :param bool get_remainder: returns a tuple with the second part being the
+    cropped portion of the message
+
+  :returns: **str** of the text truncated to the given length
   """
 
   # checks if there's room for the whole message
@@ -512,54 +559,3 @@ def is_wide_characters_supported():
     pass
 
   return False
-
-
-def _init_colors():
-  """
-  Initializes color mappings usable by curses. This can only be done after
-  calling curses.initscr().
-  """
-
-  global COLOR_ATTR_INITIALIZED, COLOR_IS_SUPPORTED
-
-  if not COLOR_ATTR_INITIALIZED:
-    # hack to replace all ACS characters with '+' if ACS support has been
-    # manually disabled
-
-    if not CONFIG["features.acsSupport"]:
-      for item in curses.__dict__:
-        if item.startswith("ACS_"):
-          curses.__dict__[item] = ord('+')
-
-      # replace a few common border pipes that are better rendered as '|' or
-      # '-' instead
-
-      curses.ACS_SBSB = ord('|')
-      curses.ACS_VLINE = ord('|')
-      curses.ACS_BSBS = ord('-')
-      curses.ACS_HLINE = ord('-')
-
-    COLOR_ATTR_INITIALIZED = True
-    COLOR_IS_SUPPORTED = False
-
-    if not CONFIG["features.colorInterface"]:
-      return
-
-    try:
-      COLOR_IS_SUPPORTED = curses.has_colors()
-    except curses.error:
-      return  # initscr hasn't been called yet
-
-    # initializes color mappings if color support is available
-    if COLOR_IS_SUPPORTED:
-      colorpair = 0
-      log.info("Terminal color support detected and enabled")
-
-      for color_name in COLOR_LIST:
-        foreground_color = COLOR_LIST[color_name]
-        background_color = -1  # allows for default (possibly transparent) background
-        colorpair += 1
-        curses.init_pair(colorpair, foreground_color, background_color)
-        COLOR_ATTR[color_name] = curses.color_pair(colorpair)
-    else:
-      log.info("Terminal color support unavailable")



More information about the tor-commits mailing list