commit 7a15738d01cb62e47e776ed41e4fb0a227f33dbb Author: Damian Johnson atagar@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")
tor-commits@lists.torproject.org