diff --git a/fill3.py b/fill3.py index 3f26040..332a832 100644 --- a/fill3.py +++ b/fill3.py @@ -457,6 +457,6 @@ def main(loop, appearance_changed_event, screen_widget, exit_loop=None): loop.add_signal_handler(signal.SIGTERM, exit_loop) asyncio.ensure_future(_update_screen(screen_widget, appearance_changed_event)) - with terminal.hidden_cursor(), _urwid_screen() as urwid_screen: + with terminal.hidden_cursor(), terminal.fullscreen(), _urwid_screen() as urwid_screen: loop.add_reader(sys.stdin, on_input, urwid_screen, screen_widget) loop.run_forever() diff --git a/terminal.py b/terminal.py index c073c50..065f8d5 100644 --- a/terminal.py +++ b/terminal.py @@ -56,17 +56,11 @@ def move(x, y): # cup @contextlib.contextmanager def fullscreen(): - if enter_fullscreen is None: - try: - yield - finally: - sys.stdout.write(clear) - else: - sys.stdout.write(enter_fullscreen) - try: - yield - finally: - sys.stdout.write(exit_fullscreen) + sys.stdout.write(enter_fullscreen) + try: + yield + finally: + sys.stdout.write(exit_fullscreen) @contextlib.contextmanager diff --git a/urwid/display_common.py b/urwid/display_common.py index 326f108..fda18f9 100644 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -20,639 +20,9 @@ import os import sys +import termios -try: - import termios -except ImportError: - pass # windows - -from urwid.util import StoppingContext, int_scale -from urwid import signals -from urwid.compat import B, bytes3 - -# for replacing unprintable bytes with '?' -UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(list(range(32,256))) - - -# signals sent by BaseScreen -UPDATE_PALETTE_ENTRY = "update palette entry" -INPUT_DESCRIPTORS_CHANGED = "input descriptors changed" - - -# AttrSpec internal values -_BASIC_START = 0 # first index of basic color aliases -_CUBE_START = 16 # first index of color cube -_CUBE_SIZE_256 = 6 # one side of the color cube -_GRAY_SIZE_256 = 24 -_GRAY_START_256 = _CUBE_SIZE_256 ** 3 + _CUBE_START -_CUBE_WHITE_256 = _GRAY_START_256 -1 -_CUBE_SIZE_88 = 4 -_GRAY_SIZE_88 = 8 -_GRAY_START_88 = _CUBE_SIZE_88 ** 3 + _CUBE_START -_CUBE_WHITE_88 = _GRAY_START_88 -1 -_CUBE_BLACK = _CUBE_START - -# values copied from xterm 256colres.h: -_CUBE_STEPS_256 = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] -_GRAY_STEPS_256 = [0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, - 0x6c, 0x76, 0x80, 0x84, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, - 0xda, 0xe4, 0xee] -# values copied from xterm 88colres.h: -_CUBE_STEPS_88 = [0x00, 0x8b, 0xcd, 0xff] -_GRAY_STEPS_88 = [0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7] -# values copied from X11/rgb.txt and XTerm-col.ad: -_BASIC_COLOR_VALUES = [(0,0,0), (205, 0, 0), (0, 205, 0), (205, 205, 0), - (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), - (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), - (0x5c, 0x5c, 0xff), (255, 0, 255), (0, 255, 255), (255, 255, 255)] - -_COLOR_VALUES_256 = (_BASIC_COLOR_VALUES + - [(r, g, b) for r in _CUBE_STEPS_256 for g in _CUBE_STEPS_256 - for b in _CUBE_STEPS_256] + - [(gr, gr, gr) for gr in _GRAY_STEPS_256]) -_COLOR_VALUES_88 = (_BASIC_COLOR_VALUES + - [(r, g, b) for r in _CUBE_STEPS_88 for g in _CUBE_STEPS_88 - for b in _CUBE_STEPS_88] + - [(gr, gr, gr) for gr in _GRAY_STEPS_88]) - -assert len(_COLOR_VALUES_256) == 256 -assert len(_COLOR_VALUES_88) == 88 - -_FG_COLOR_MASK = 0x000000ff -_BG_COLOR_MASK = 0x0000ff00 -_FG_BASIC_COLOR = 0x00010000 -_FG_HIGH_COLOR = 0x00020000 -_BG_BASIC_COLOR = 0x00040000 -_BG_HIGH_COLOR = 0x00080000 -_BG_SHIFT = 8 -_HIGH_88_COLOR = 0x00100000 -_STANDOUT = 0x02000000 -_UNDERLINE = 0x04000000 -_BOLD = 0x08000000 -_BLINK = 0x10000000 -_FG_MASK = (_FG_COLOR_MASK | _FG_BASIC_COLOR | _FG_HIGH_COLOR | - _STANDOUT | _UNDERLINE | _BLINK | _BOLD) -_BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR - -DEFAULT = 'default' -BLACK = 'black' -DARK_RED = 'dark red' -DARK_GREEN = 'dark green' -BROWN = 'brown' -DARK_BLUE = 'dark blue' -DARK_MAGENTA = 'dark magenta' -DARK_CYAN = 'dark cyan' -LIGHT_GRAY = 'light gray' -DARK_GRAY = 'dark gray' -LIGHT_RED = 'light red' -LIGHT_GREEN = 'light green' -YELLOW = 'yellow' -LIGHT_BLUE = 'light blue' -LIGHT_MAGENTA = 'light magenta' -LIGHT_CYAN = 'light cyan' -WHITE = 'white' - -_BASIC_COLORS = [ - BLACK, - DARK_RED, - DARK_GREEN, - BROWN, - DARK_BLUE, - DARK_MAGENTA, - DARK_CYAN, - LIGHT_GRAY, - DARK_GRAY, - LIGHT_RED, - LIGHT_GREEN, - YELLOW, - LIGHT_BLUE, - LIGHT_MAGENTA, - LIGHT_CYAN, - WHITE, -] - -_ATTRIBUTES = { - 'bold': _BOLD, - 'underline': _UNDERLINE, - 'blink': _BLINK, - 'standout': _STANDOUT, -} - -def _value_lookup_table(values, size): - """ - Generate a lookup table for finding the closest item in values. - Lookup returns (index into values)+1 - - values -- list of values in ascending order, all < size - size -- size of lookup table and maximum value - - >>> _value_lookup_table([0, 7, 9], 10) - [0, 0, 0, 0, 1, 1, 1, 1, 2, 2] - """ - - middle_values = [0] + [(values[i] + values[i + 1] + 1) // 2 - for i in range(len(values) - 1)] + [size] - lookup_table = [] - for i in range(len(middle_values)-1): - count = middle_values[i + 1] - middle_values[i] - lookup_table.extend([i] * count) - return lookup_table - -_CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256) -_GRAY_256_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_256 + [0xff], 256) -_CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256) -_GRAY_88_LOOKUP = _value_lookup_table([0] + _GRAY_STEPS_88 + [0xff], 256) - -# convert steps to values that will be used by string versions of the colors -# 1 hex digit for rgb and 0..100 for grayscale -_CUBE_STEPS_256_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_256] -_GRAY_STEPS_256_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_256] -_CUBE_STEPS_88_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_88] -_GRAY_STEPS_88_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_88] - -# create lookup tables for 1 hex digit rgb and 0..100 for grayscale values -_CUBE_256_LOOKUP_16 = [_CUBE_256_LOOKUP[int_scale(n, 16, 0x100)] - for n in range(16)] -_GRAY_256_LOOKUP_101 = [_GRAY_256_LOOKUP[int_scale(n, 101, 0x100)] - for n in range(101)] -_CUBE_88_LOOKUP_16 = [_CUBE_88_LOOKUP[int_scale(n, 16, 0x100)] - for n in range(16)] -_GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)] - for n in range(101)] - - -# The functions _gray_num_256() and _gray_num_88() do not include the gray -# values from the color cube so that the gray steps are an even width. -# The color cube grays are available by using the rgb functions. Pure -# white and black are taken from the color cube, since the gray range does -# not include them, and the basic colors are more likely to have been -# customized by an end-user. - - -def _gray_num_256(gnum): - """Return ths color number for gray number gnum. - - Color cube black and white are returned for 0 and 25 respectively - since those values aren't included in the gray scale. - - """ - # grays start from index 1 - gnum -= 1 - - if gnum < 0: - return _CUBE_BLACK - if gnum >= _GRAY_SIZE_256: - return _CUBE_WHITE_256 - return _GRAY_START_256 + gnum - - -def _gray_num_88(gnum): - """Return ths color number for gray number gnum. - - Color cube black and white are returned for 0 and 9 respectively - since those values aren't included in the gray scale. - - """ - # gnums start from index 1 - gnum -= 1 - - if gnum < 0: - return _CUBE_BLACK - if gnum >= _GRAY_SIZE_88: - return _CUBE_WHITE_88 - return _GRAY_START_88 + gnum - - -def _color_desc_256(num): - """ - Return a string description of color number num. - 0..15 -> 'h0'..'h15' basic colors (as high-colors) - 16..231 -> '#000'..'#fff' color cube colors - 232..255 -> 'g3'..'g93' grays - - >>> _color_desc_256(15) - 'h15' - >>> _color_desc_256(16) - '#000' - >>> _color_desc_256(17) - '#006' - >>> _color_desc_256(230) - '#ffd' - >>> _color_desc_256(233) - 'g7' - >>> _color_desc_256(234) - 'g11' - - """ - assert num >= 0 and num < 256, num - if num < _CUBE_START: - return 'h%d' % num - if num < _GRAY_START_256: - num -= _CUBE_START - b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 - g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 - r = num % _CUBE_SIZE_256 - return '#%x%x%x' % (_CUBE_STEPS_256_16[r], _CUBE_STEPS_256_16[g], - _CUBE_STEPS_256_16[b]) - return 'g%d' % _GRAY_STEPS_256_101[num - _GRAY_START_256] - -def _color_desc_88(num): - """ - Return a string description of color number num. - 0..15 -> 'h0'..'h15' basic colors (as high-colors) - 16..79 -> '#000'..'#fff' color cube colors - 80..87 -> 'g18'..'g90' grays - - >>> _color_desc_88(15) - 'h15' - >>> _color_desc_88(16) - '#000' - >>> _color_desc_88(17) - '#008' - >>> _color_desc_88(78) - '#ffc' - >>> _color_desc_88(81) - 'g36' - >>> _color_desc_88(82) - 'g45' - - """ - assert num > 0 and num < 88 - if num < _CUBE_START: - return 'h%d' % num - if num < _GRAY_START_88: - num -= _CUBE_START - b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - g, r= num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 - return '#%x%x%x' % (_CUBE_STEPS_88_16[r], _CUBE_STEPS_88_16[g], - _CUBE_STEPS_88_16[b]) - return 'g%d' % _GRAY_STEPS_88_101[num - _GRAY_START_88] - -def _parse_color_256(desc): - """ - Return a color number for the description desc. - 'h0'..'h255' -> 0..255 actual color number - '#000'..'#fff' -> 16..231 color cube colors - 'g0'..'g100' -> 16, 232..255, 231 grays and color cube black/white - 'g#00'..'g#ff' -> 16, 232...255, 231 gray and color cube black/white - - Returns None if desc is invalid. - - >>> _parse_color_256('h142') - 142 - >>> _parse_color_256('#f00') - 196 - >>> _parse_color_256('g100') - 231 - >>> _parse_color_256('g#80') - 244 - """ - if len(desc) > 4: - # keep the length within reason before parsing - return None - try: - if desc.startswith('h'): - # high-color number - num = int(desc[1:], 10) - if num < 0 or num > 255: - return None - return num - - if desc.startswith('#') and len(desc) == 4: - # color-cube coordinates - rgb = int(desc[1:], 16) - if rgb < 0: - return None - b, rgb = rgb % 16, rgb // 16 - g, r = rgb % 16, rgb // 16 - # find the closest rgb values - r = _CUBE_256_LOOKUP_16[r] - g = _CUBE_256_LOOKUP_16[g] - b = _CUBE_256_LOOKUP_16[b] - return _CUBE_START + (r * _CUBE_SIZE_256 + g) * _CUBE_SIZE_256 + b - - # Only remaining possibility is gray value - if desc.startswith('g#'): - # hex value 00..ff - gray = int(desc[2:], 16) - if gray < 0 or gray > 255: - return None - gray = _GRAY_256_LOOKUP[gray] - elif desc.startswith('g'): - # decimal value 0..100 - gray = int(desc[1:], 10) - if gray < 0 or gray > 100: - return None - gray = _GRAY_256_LOOKUP_101[gray] - else: - return None - if gray == 0: - return _CUBE_BLACK - gray -= 1 - if gray == _GRAY_SIZE_256: - return _CUBE_WHITE_256 - return _GRAY_START_256 + gray - - except ValueError: - return None - -def _parse_color_88(desc): - """ - Return a color number for the description desc. - 'h0'..'h87' -> 0..87 actual color number - '#000'..'#fff' -> 16..79 color cube colors - 'g0'..'g100' -> 16, 80..87, 79 grays and color cube black/white - 'g#00'..'g#ff' -> 16, 80...87, 79 gray and color cube black/white - - Returns None if desc is invalid. - - >>> _parse_color_88('h142') - >>> _parse_color_88('h42') - 42 - >>> _parse_color_88('#f00') - 64 - >>> _parse_color_88('g100') - 79 - >>> _parse_color_88('g#80') - 83 - """ - if len(desc) > 4: - # keep the length within reason before parsing - return None - try: - if desc.startswith('h'): - # high-color number - num = int(desc[1:], 10) - if num < 0 or num > 87: - return None - return num - - if desc.startswith('#') and len(desc) == 4: - # color-cube coordinates - rgb = int(desc[1:], 16) - if rgb < 0: - return None - b, rgb = rgb % 16, rgb // 16 - g, r = rgb % 16, rgb // 16 - # find the closest rgb values - r = _CUBE_88_LOOKUP_16[r] - g = _CUBE_88_LOOKUP_16[g] - b = _CUBE_88_LOOKUP_16[b] - return _CUBE_START + (r * _CUBE_SIZE_88 + g) * _CUBE_SIZE_88 + b - - # Only remaining possibility is gray value - if desc.startswith('g#'): - # hex value 00..ff - gray = int(desc[2:], 16) - if gray < 0 or gray > 255: - return None - gray = _GRAY_88_LOOKUP[gray] - elif desc.startswith('g'): - # decimal value 0..100 - gray = int(desc[1:], 10) - if gray < 0 or gray > 100: - return None - gray = _GRAY_88_LOOKUP_101[gray] - else: - return None - if gray == 0: - return _CUBE_BLACK - gray -= 1 - if gray == _GRAY_SIZE_88: - return _CUBE_WHITE_88 - return _GRAY_START_88 + gray - - except ValueError: - return None - -class AttrSpecError(Exception): - pass - -class AttrSpec(object): - def __init__(self, fg, bg, colors=256): - """ - fg -- a string containing a comma-separated foreground color - and settings - - Color values: - 'default' (use the terminal's default foreground), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray', 'dark gray', - 'light red', 'light green', 'yellow', 'light blue', - 'light magenta', 'light cyan', 'white' - - High-color example values: - '#009' (0% red, 0% green, 60% red, like HTML colors) - '#fcc' (100% red, 80% green, 80% blue) - 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), - '#000', 'g0', 'g#00' (black), - '#fff', 'g100', 'g#ff' (white) - 'h8' (color number 8), 'h255' (color number 255) - - Setting: - 'bold', 'underline', 'blink', 'standout' - - Some terminals use 'bold' for bright colors. Most terminals - ignore the 'blink' setting. If the color is not given then - 'default' will be assumed. - - bg -- a string containing the background color - - Color values: - 'default' (use the terminal's default background), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray' - - High-color exaples: - see fg examples above - - An empty string will be treated the same as 'default'. - - colors -- the maximum colors available for the specification - - Valid values include: 1, 16, 88 and 256. High-color - values are only usable with 88 or 256 colors. With - 1 color only the foreground settings may be used. - - >>> AttrSpec('dark red', 'light gray', 16) - AttrSpec('dark red', 'light gray') - >>> AttrSpec('yellow, underline, bold', 'dark blue') - AttrSpec('yellow,bold,underline', 'dark blue') - >>> AttrSpec('#ddb', '#004', 256) # closest colors will be found - AttrSpec('#dda', '#006') - >>> AttrSpec('#ddb', '#004', 88) - AttrSpec('#ccc', '#000', colors=88) - """ - if colors not in (1, 16, 88, 256): - raise AttrSpecError('invalid number of colors (%d).' % colors) - self._value = 0 | _HIGH_88_COLOR * (colors == 88) - self.foreground = fg - self.background = bg - if self.colors > colors: - raise AttrSpecError(('foreground/background (%s/%s) require ' + - 'more colors than have been specified (%d).') % - (repr(fg), repr(bg), colors)) - - foreground_basic = property(lambda s: s._value & _FG_BASIC_COLOR != 0) - foreground_high = property(lambda s: s._value & _FG_HIGH_COLOR != 0) - foreground_number = property(lambda s: s._value & _FG_COLOR_MASK) - background_basic = property(lambda s: s._value & _BG_BASIC_COLOR != 0) - background_high = property(lambda s: s._value & _BG_HIGH_COLOR != 0) - background_number = property(lambda s: (s._value & _BG_COLOR_MASK) - >> _BG_SHIFT) - bold = property(lambda s: s._value & _BOLD != 0) - underline = property(lambda s: s._value & _UNDERLINE != 0) - blink = property(lambda s: s._value & _BLINK != 0) - standout = property(lambda s: s._value & _STANDOUT != 0) - - def _colors(self): - """ - Return the maximum colors required for this object. - - Returns 256, 88, 16 or 1. - """ - if self._value & _HIGH_88_COLOR: - return 88 - if self._value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR): - return 256 - if self._value & (_BG_BASIC_COLOR | _BG_BASIC_COLOR): - return 16 - return 1 - colors = property(_colors) - - def __repr__(self): - """ - Return an executable python representation of the AttrSpec - object. - """ - args = "%r, %r" % (self.foreground, self.background) - if self.colors == 88: - # 88-color mode is the only one that is handled differently - args = args + ", colors=88" - return "%s(%s)" % (self.__class__.__name__, args) - - def _foreground_color(self): - """Return only the color component of the foreground.""" - if not (self.foreground_basic or self.foreground_high): - return 'default' - if self.foreground_basic: - return _BASIC_COLORS[self.foreground_number] - if self.colors == 88: - return _color_desc_88(self.foreground_number) - return _color_desc_256(self.foreground_number) - - def _foreground(self): - return (self._foreground_color() + - ',bold' * self.bold + ',standout' * self.standout + - ',blink' * self.blink + ',underline' * self.underline) - - def _set_foreground(self, foreground): - color = None - flags = 0 - # handle comma-separated foreground - for part in foreground.split(','): - part = part.strip() - if part in _ATTRIBUTES: - # parse and store "settings"/attributes in flags - if flags & _ATTRIBUTES[part]: - raise AttrSpecError(("Setting %s specified more than" + - "once in foreground (%s)") % (repr(part), - repr(foreground))) - flags |= _ATTRIBUTES[part] - continue - # past this point we must be specifying a color - if part in ('', 'default'): - scolor = 0 - elif part in _BASIC_COLORS: - scolor = _BASIC_COLORS.index(part) - flags |= _FG_BASIC_COLOR - elif self._value & _HIGH_88_COLOR: - scolor = _parse_color_88(part) - flags |= _FG_HIGH_COLOR - else: - scolor = _parse_color_256(part) - flags |= _FG_HIGH_COLOR - # _parse_color_*() return None for unrecognised colors - if scolor is None: - raise AttrSpecError(("Unrecognised color specification %s " + - "in foreground (%s)") % (repr(part), repr(foreground))) - if color is not None: - raise AttrSpecError(("More than one color given for " + - "foreground (%s)") % (repr(foreground),)) - color = scolor - if color is None: - color = 0 - self._value = (self._value & ~_FG_MASK) | color | flags - - foreground = property(_foreground, _set_foreground) - - def _background(self): - """Return the background color.""" - if not (self.background_basic or self.background_high): - return 'default' - if self.background_basic: - return _BASIC_COLORS[self.background_number] - if self._value & _HIGH_88_COLOR: - return _color_desc_88(self.background_number) - return _color_desc_256(self.background_number) - - def _set_background(self, background): - flags = 0 - if background in ('', 'default'): - color = 0 - elif background in _BASIC_COLORS: - color = _BASIC_COLORS.index(background) - flags |= _BG_BASIC_COLOR - elif self._value & _HIGH_88_COLOR: - color = _parse_color_88(background) - flags |= _BG_HIGH_COLOR - else: - color = _parse_color_256(background) - flags |= _BG_HIGH_COLOR - if color is None: - raise AttrSpecError(("Unrecognised color specification " + - "in background (%s)") % (repr(background),)) - self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags - - background = property(_background, _set_background) - - def get_rgb_values(self): - """ - Return (fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue) color - components. Each component is in the range 0-255. Values are taken - from the XTerm defaults and may not exactly match the user's terminal. - - If the foreground or background is 'default' then all their compenents - will be returned as None. - - >>> AttrSpec('yellow', '#ccf', colors=88).get_rgb_values() - (255, 255, 0, 205, 205, 255) - >>> AttrSpec('default', 'g92').get_rgb_values() - (None, None, None, 238, 238, 238) - """ - if not (self.foreground_basic or self.foreground_high): - vals = (None, None, None) - elif self.colors == 88: - assert self.foreground_number < 88, "Invalid AttrSpec _value" - vals = _COLOR_VALUES_88[self.foreground_number] - else: - vals = _COLOR_VALUES_256[self.foreground_number] - - if not (self.background_basic or self.background_high): - return vals + (None, None, None) - elif self.colors == 88: - assert self.background_number < 88, "Invalid AttrSpec _value" - return vals + _COLOR_VALUES_88[self.background_number] - else: - return vals + _COLOR_VALUES_256[self.background_number] - - def __eq__(self, other): - return isinstance(other, AttrSpec) and self._value == other._value - - def __ne__(self, other): - return not self == other - - __hash__ = object.__hash__ +from urwid.util import StoppingContext class RealTerminal(object): @@ -711,11 +81,10 @@ class RealTerminal(object): class ScreenError(Exception): pass -class BaseScreen(object, metaclass=signals.MetaSignals): +class BaseScreen(): """ Base class for Screen classes (raw_display.Screen, .. etc) """ - signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED] def __init__(self): super(BaseScreen,self).__init__() @@ -753,137 +122,6 @@ class BaseScreen(object, metaclass=signals.MetaSignals): def _stop(self): pass - def run_wrapper(self, fn, *args, **kwargs): - """Start the screen, call a function, then stop the screen. Extra - arguments are passed to `start`. - - Deprecated in favor of calling `start` as a context manager. - """ - with self.start(*args, **kwargs): - return fn() - - - def register_palette(self, palette): - """Register a set of palette entries. - - palette -- a list of (name, like_other_name) or - (name, foreground, background, mono, foreground_high, - background_high) tuples - - The (name, like_other_name) format will copy the settings - from the palette entry like_other_name, which must appear - before this tuple in the list. - - The mono and foreground/background_high values are - optional ie. the second tuple format may have 3, 4 or 6 - values. See register_palette_entry() for a description - of the tuple values. - """ - - for item in palette: - if len(item) in (3,4,6): - self.register_palette_entry(*item) - continue - if len(item) != 2: - raise ScreenError("Invalid register_palette entry: %s" % - repr(item)) - name, like_name = item - if like_name not in self._palette: - raise ScreenError("palette entry '%s' doesn't exist"%like_name) - self._palette[name] = self._palette[like_name] - - def register_palette_entry(self, name, foreground, background, - mono=None, foreground_high=None, background_high=None): - """Register a single palette entry. - - name -- new entry/attribute name - - foreground -- a string containing a comma-separated foreground - color and settings - - Color values: - 'default' (use the terminal's default foreground), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray', 'dark gray', - 'light red', 'light green', 'yellow', 'light blue', - 'light magenta', 'light cyan', 'white' - - Settings: - 'bold', 'underline', 'blink', 'standout' - - Some terminals use 'bold' for bright colors. Most terminals - ignore the 'blink' setting. If the color is not given then - 'default' will be assumed. - - background -- a string containing the background color - - Background color values: - 'default' (use the terminal's default background), - 'black', 'dark red', 'dark green', 'brown', 'dark blue', - 'dark magenta', 'dark cyan', 'light gray' - - mono -- a comma-separated string containing monochrome terminal - settings (see "Settings" above.) - - None = no terminal settings (same as 'default') - - foreground_high -- a string containing a comma-separated - foreground color and settings, standard foreground - colors (see "Color values" above) or high-colors may - be used - - High-color example values: - '#009' (0% red, 0% green, 60% red, like HTML colors) - '#fcc' (100% red, 80% green, 80% blue) - 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), - '#000', 'g0', 'g#00' (black), - '#fff', 'g100', 'g#ff' (white) - 'h8' (color number 8), 'h255' (color number 255) - - None = use foreground parameter value - - background_high -- a string containing the background color, - standard background colors (see "Background colors" above) - or high-colors (see "High-color example values" above) - may be used - - None = use background parameter value - """ - basic = AttrSpec(foreground, background, 16) - - if type(mono) == tuple: - # old style of specifying mono attributes was to put them - # in a tuple. convert to comma-separated string - mono = ",".join(mono) - if mono is None: - mono = DEFAULT - mono = AttrSpec(mono, DEFAULT, 1) - - if foreground_high is None: - foreground_high = foreground - if background_high is None: - background_high = background - high_256 = AttrSpec(foreground_high, background_high, 256) - - # 'hX' where X > 15 are different in 88/256 color, use - # basic colors for 88-color mode if high colors are specified - # in this way (also avoids crash when X > 87) - def large_h(desc): - if not desc.startswith('h'): - return False - if ',' in desc: - desc = desc.split(',',1)[0] - num = int(desc[1:], 10) - return num > 15 - if large_h(foreground_high) or large_h(background_high): - high_88 = basic - else: - high_88 = AttrSpec(foreground_high, background_high, 88) - - signals.emit_signal(self, UPDATE_PALETTE_ENTRY, - name, basic, mono, high_88, high_256) - self._palette[name] = (basic, mono, high_88, high_256) - def _test(): import doctest diff --git a/urwid/raw_display.py b/urwid/raw_display.py index abc8a34..8c12fbe 100644 --- a/urwid/raw_display.py +++ b/urwid/raw_display.py @@ -25,23 +25,13 @@ Direct terminal UI implementation import os import select -import struct import sys -import signal - -import fcntl import termios import tty -from urwid import util from urwid import escape -from urwid.display_common import BaseScreen, RealTerminal, \ - UPDATE_PALETTE_ENTRY, AttrSpec, UNPRINTABLE_TRANS_TABLE, \ - INPUT_DESCRIPTORS_CHANGED -from urwid import signals -from urwid.compat import PYTHON3, bytes, B -from subprocess import Popen, PIPE +from urwid.display_common import BaseScreen, RealTerminal class Screen(BaseScreen, RealTerminal): @@ -50,47 +40,16 @@ class Screen(BaseScreen, RealTerminal): terminal. """ super(Screen, self).__init__() - self._pal_escape = {} - self._pal_attrspec = {} - signals.connect_signal(self, UPDATE_PALETTE_ENTRY, - self._on_update_palette_entry) - self.colors = 16 # FIXME: detect this - self.has_underline = True # FIXME: detect this - self.register_palette_entry( None, 'default','default') self._keyqueue = [] self.prev_input_resize = 0 self.set_input_timeouts() - self.screen_buf = None - self._screen_buf_canvas = None - self._resized = False - self.maxrow = None - self.gpm_mev = None - self.gpm_event_pending = False self._mouse_tracking_enabled = False - self.last_bstate = 0 - self._setup_G1_done = False - self._rows_used = None - self._cy = 0 - term = os.environ.get('TERM', '') - self.fg_bright_is_bold = not term.startswith("xterm") - self.bg_bright_is_blink = (term == "linux") - self.back_color_erase = not term.startswith("screen") self._next_timeout = None # Our connections to the world self._term_output_file = output self._term_input_file = input - # pipe for signalling external event loops about resize events - self._resize_pipe_rd, self._resize_pipe_wr = os.pipe() - fcntl.fcntl(self._resize_pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK) - - def _on_update_palette_entry(self, name, *attrspecs): - # copy the attribute to a dictionary containing the escape seqences - a = attrspecs[{16:0,1:1,88:2,256:3}[self.colors]] - self._pal_attrspec[name] = a - self._pal_escape[name] = self._attrspec_to_escape(a) - def set_input_timeouts(self, max_wait=None, complete_wait=0.125, resize_wait=0.125): """ @@ -116,39 +75,6 @@ class Screen(BaseScreen, RealTerminal): self.complete_wait = complete_wait self.resize_wait = resize_wait - def _sigwinch_handler(self, signum, frame): - if not self._resized: - os.write(self._resize_pipe_wr, B('R')) - self._resized = True - self.screen_buf = None - - def _sigcont_handler(self, signum, frame): - self.stop() - self.start() - self._sigwinch_handler(None, None) - - def signal_init(self): - """ - Called in the startup of run wrapper to set the SIGWINCH - and SIGCONT signal handlers. - - Override this function to call from main thread in threaded - applications. - """ - signal.signal(signal.SIGWINCH, self._sigwinch_handler) - signal.signal(signal.SIGCONT, self._sigcont_handler) - - def signal_restore(self): - """ - Called in the finally block of run wrapper to restore the - SIGWINCH and SIGCONT signal handlers. - - Override this function to call from main thread in threaded - applications. - """ - signal.signal(signal.SIGCONT, signal.SIG_DFL) - signal.signal(signal.SIGWINCH, signal.SIG_DFL) - def set_mouse_tracking(self, enable=True): """ Enable (or disable) mouse tracking. @@ -166,29 +92,8 @@ class Screen(BaseScreen, RealTerminal): def _mouse_tracking(self, enable): if enable: self.write(escape.MOUSE_TRACKING_ON) - self._start_gpm_tracking() else: self.write(escape.MOUSE_TRACKING_OFF) - self._stop_gpm_tracking() - - def _start_gpm_tracking(self): - if not os.path.isfile("/usr/bin/mev"): - return - if not os.environ.get('TERM',"").lower().startswith("linux"): - return - if not Popen: - return - m = Popen(["/usr/bin/mev","-e","158"], stdin=PIPE, stdout=PIPE, - close_fds=True) - fcntl.fcntl(m.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK) - self.gpm_mev = m - - def _stop_gpm_tracking(self): - if not self.gpm_mev: - return - os.kill(self.gpm_mev.pid, signal.SIGINT) - os.waitpid(self.gpm_mev.pid, 0) - self.gpm_mev = None def _start(self, alternate_buffer=True): """ @@ -207,15 +112,12 @@ class Screen(BaseScreen, RealTerminal): self._old_termios_settings = termios.tcgetattr(fd) tty.setcbreak(fd) - self.signal_init() self._alternate_buffer = alternate_buffer self._next_timeout = self.max_wait if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys(fileno=fd) - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - # restore mouse tracking to previous state self._mouse_tracking(self._mouse_tracking_enabled) return super(Screen, self)._start() @@ -224,11 +126,6 @@ class Screen(BaseScreen, RealTerminal): """ Restore the screen. """ - self.clear() - - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - - self.signal_restore() fd = self._term_input_file.fileno() if os.isatty(fd): @@ -240,12 +137,9 @@ class Screen(BaseScreen, RealTerminal): move_cursor = "" if self._alternate_buffer: move_cursor = escape.RESTORE_NORMAL_BUFFER - elif self.maxrow is not None: - move_cursor = escape.set_cursor_position( - 0, self.maxrow) + self.write( - self._attrspec_to_escape(AttrSpec('','')) - + escape.SI + escape.SI + move_cursor + escape.SHOW_CURSOR) self.flush() @@ -347,73 +241,10 @@ class Screen(BaseScreen, RealTerminal): return keys, raw return keys - def get_input_descriptors(self): - """ - Return a list of integer file descriptors that should be - polled in external event loops to check for user input. - - Use this method if you are implementing your own event loop. - """ - if not self._started: - return [] - - fd_list = [self._term_input_file.fileno(), self._resize_pipe_rd] - if self.gpm_mev is not None: - fd_list.append(self.gpm_mev.stdout.fileno()) - return fd_list - - _current_event_loop_handles = () - - def unhook_event_loop(self, event_loop): - """ - Remove any hooks added by hook_event_loop. - """ - for handle in self._current_event_loop_handles: - event_loop.remove_watch_file(handle) - - if self._input_timeout: - event_loop.remove_alarm(self._input_timeout) - self._input_timeout = None - - def hook_event_loop(self, event_loop, callback): - """ - Register the given callback with the event loop, to be called with new - input whenever it's available. The callback should be passed a list of - processed keys and a list of unprocessed keycodes. - - Subclasses may wish to use parse_input to wrap the callback. - """ - if hasattr(self, 'get_input_nonblocking'): - wrapper = self._make_legacy_input_wrapper(event_loop, callback) - else: - wrapper = lambda: self.parse_input( - event_loop, callback, self.get_available_raw_input()) - fds = self.get_input_descriptors() - handles = [] - for fd in fds: - event_loop.watch_file(fd, wrapper) - self._current_event_loop_handles = handles _input_timeout = None _partial_codes = None - def _make_legacy_input_wrapper(self, event_loop, callback): - """ - Support old Screen classes that still have a get_input_nonblocking and - expect it to work. - """ - def wrapper(): - if self._input_timeout: - event_loop.remove_alarm(self._input_timeout) - self._input_timeout = None - timeout, keys, raw = self.get_input_nonblocking() - if timeout is not None: - self._input_timeout = event_loop.alarm(timeout, wrapper) - - callback(keys, raw) - - return wrapper - def get_available_raw_input(self): """ Return any currently-available input. Does not block. @@ -421,19 +252,12 @@ class Screen(BaseScreen, RealTerminal): This method is only used by the default `hook_event_loop` implementation; you can safely ignore it if you implement your own. """ - codes = self._get_gpm_codes() + self._get_keyboard_codes() + codes = self._get_keyboard_codes() if self._partial_codes: codes = self._partial_codes + codes self._partial_codes = None - # clean out the pipe used to signal external event loops - # that a resize has occurred - try: - while True: os.read(self._resize_pipe_rd, 1) - except OSError: - pass - return codes def parse_input(self, event_loop, callback, codes, wait_for_more=True): @@ -482,10 +306,6 @@ class Screen(BaseScreen, RealTerminal): processed_codes = original_codes self._partial_codes = None - if self._resized: - processed.append('window resize') - self._resized = False - if callback: callback(processed, processed_codes) else: @@ -501,21 +321,9 @@ class Screen(BaseScreen, RealTerminal): codes.append(code) return codes - def _get_gpm_codes(self): - codes = [] - try: - while self.gpm_mev is not None and self.gpm_event_pending: - codes.extend(self._encode_gpm_event()) - except IOError as e: - if e.args[0] != 11: - raise - return codes - def _wait_for_input_ready(self, timeout): ready = None fd_list = [self._term_input_file.fileno()] - if self.gpm_mev is not None: - fd_list.append(self.gpm_mev.stdout.fileno()) while True: try: if timeout is None: @@ -528,498 +336,18 @@ class Screen(BaseScreen, RealTerminal): except select.error as e: if e.args[0] != 4: raise - if self._resized: - ready = [] - break return ready def _getch(self, timeout): ready = self._wait_for_input_ready(timeout) - if self.gpm_mev is not None: - if self.gpm_mev.stdout.fileno() in ready: - self.gpm_event_pending = True if self._term_input_file.fileno() in ready: return ord(os.read(self._term_input_file.fileno(), 1)) return -1 - def _encode_gpm_event( self ): - self.gpm_event_pending = False - s = self.gpm_mev.stdout.readline().decode('ascii') - l = s.split(",") - if len(l) != 6: - # unexpected output, stop tracking - self._stop_gpm_tracking() - signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) - return [] - ev, x, y, ign, b, m = s.split(",") - ev = int( ev.split("x")[-1], 16) - x = int( x.split(" ")[-1] ) - y = int( y.lstrip().split(" ")[0] ) - b = int( b.split(" ")[-1] ) - m = int( m.split("x")[-1].rstrip(), 16 ) - - # convert to xterm-like escape sequence - - last = next = self.last_bstate - l = [] - - mod = 0 - if m & 1: mod |= 4 # shift - if m & 10: mod |= 8 # alt - if m & 4: mod |= 16 # ctrl - - def append_button( b ): - b |= mod - l.extend([ 27, ord('['), ord('M'), b+32, x+32, y+32 ]) - - def determine_button_release( flag ): - if b & 4 and last & 1: - append_button( 0 + flag ) - next |= 1 - if b & 2 and last & 2: - append_button( 1 + flag ) - next |= 2 - if b & 1 and last & 4: - append_button( 2 + flag ) - next |= 4 - - if ev == 20 or ev == 36 or ev == 52: # press - if b & 4 and last & 1 == 0: - append_button( 0 ) - next |= 1 - if b & 2 and last & 2 == 0: - append_button( 1 ) - next |= 2 - if b & 1 and last & 4 == 0: - append_button( 2 ) - next |= 4 - elif ev == 146: # drag - if b & 4: - append_button( 0 + escape.MOUSE_DRAG_FLAG ) - elif b & 2: - append_button( 1 + escape.MOUSE_DRAG_FLAG ) - elif b & 1: - append_button( 2 + escape.MOUSE_DRAG_FLAG ) - else: # release - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 1 - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 2 - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_RELEASE_FLAG ) - next &= ~ 4 - if ev == 40: # double click (release) - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG ) - elif ev == 52: - if b & 4 and last & 1: - append_button( 0 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - if b & 2 and last & 2: - append_button( 1 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - if b & 1 and last & 4: - append_button( 2 + escape.MOUSE_MULTIPLE_CLICK_FLAG*2 ) - - self.last_bstate = next - return l - def _getch_nodelay(self): return self._getch(0) - def get_cols_rows(self): - """Return the terminal dimensions (num columns, num rows).""" - y, x = 80, 24 - try: - buf = fcntl.ioctl(self._term_output_file.fileno(), - termios.TIOCGWINSZ, ' '*4) - y, x = struct.unpack('hh', buf) - except IOError: - # Term size could not be determined - pass - self.maxrow = y - return x, y - - def _setup_G1(self): - """ - Initialize the G1 character set to graphics mode if required. - """ - if self._setup_G1_done: - return - - while True: - try: - self.write(escape.DESIGNATE_G1_SPECIAL) - self.flush() - break - except IOError: - pass - self._setup_G1_done = True - - - def draw_screen(self, xxx_todo_changeme, r ): - """Paint screen with rendered canvas.""" - (maxcol, maxrow) = xxx_todo_changeme - assert self._started - - assert maxrow == r.rows() - - # quick return if nothing has changed - if self.screen_buf and r is self._screen_buf_canvas: - return - - self._setup_G1() - - if self._resized: - # handle resize before trying to draw screen - return - - o = [escape.HIDE_CURSOR, self._attrspec_to_escape(AttrSpec('',''))] - - def partial_display(): - # returns True if the screen is in partial display mode - # ie. only some rows belong to the display - return self._rows_used is not None - - if not partial_display(): - o.append(escape.CURSOR_HOME) - - if self.screen_buf: - osb = self.screen_buf - else: - osb = [] - sb = [] - cy = self._cy - y = -1 - - def set_cursor_home(): - if not partial_display(): - return escape.set_cursor_position(0, 0) - return (escape.CURSOR_HOME_COL + - escape.move_cursor_up(cy)) - - def set_cursor_row(y): - if not partial_display(): - return escape.set_cursor_position(0, y) - return escape.move_cursor_down(y - cy) - - def set_cursor_position(x, y): - if not partial_display(): - return escape.set_cursor_position(x, y) - if cy > y: - return ('\b' + escape.CURSOR_HOME_COL + - escape.move_cursor_up(cy - y) + - escape.move_cursor_right(x)) - return ('\b' + escape.CURSOR_HOME_COL + - escape.move_cursor_down(y - cy) + - escape.move_cursor_right(x)) - - def is_blank_row(row): - if len(row) > 1: - return False - if row[0][2].strip(): - return False - return True - - def attr_to_escape(a): - if a in self._pal_escape: - return self._pal_escape[a] - elif isinstance(a, AttrSpec): - return self._attrspec_to_escape(a) - # undefined attributes use default/default - # TODO: track and report these - return self._attrspec_to_escape( - AttrSpec('default','default')) - - def using_standout(a): - a = self._pal_attrspec.get(a, a) - return isinstance(a, AttrSpec) and a.standout - - ins = None - o.append(set_cursor_home()) - cy = 0 - for row in r.content(): - y += 1 - if osb and y < len(osb) and osb[y] == row: - # this row of the screen buffer matches what is - # currently displayed, so we can skip this line - sb.append( osb[y] ) - continue - - sb.append(row) - - # leave blank lines off display when we are using - # the default screen buffer (allows partial screen) - if partial_display() and y > self._rows_used: - if is_blank_row(row): - continue - self._rows_used = y - - if y or partial_display(): - o.append(set_cursor_position(0, y)) - # after updating the line we will be just over the - # edge, but terminals still treat this as being - # on the same line - cy = y - - whitespace_at_end = False - if row: - a, cs, run = row[-1] - if (run[-1:] == B(' ') and self.back_color_erase - and not using_standout(a)): - whitespace_at_end = True - row = row[:-1] + [(a, cs, run.rstrip(B(' ')))] - elif y == maxrow-1 and maxcol > 1: - row, back, ins = self._last_row(row) - - first = True - lasta = lastcs = None - for (a,cs, run) in row: - assert isinstance(run, bytes) # canvases should render with bytes - if cs != 'U': - run = run.translate(UNPRINTABLE_TRANS_TABLE) - if first or lasta != a: - o.append(attr_to_escape(a)) - lasta = a - if first or lastcs != cs: - assert cs in [None, "0", "U"], repr(cs) - if lastcs == "U": - o.append( escape.IBMPC_OFF ) - - if cs is None: - o.append( escape.SI ) - elif cs == "U": - o.append( escape.IBMPC_ON ) - else: - o.append( escape.SO ) - lastcs = cs - o.append( run ) - first = False - if ins: - (inserta, insertcs, inserttext) = ins - ias = attr_to_escape(inserta) - assert insertcs in [None, "0", "U"], repr(insertcs) - if cs is None: - icss = escape.SI - elif cs == "U": - icss = escape.IBMPC_ON - else: - icss = escape.SO - o += [ "\x08"*back, - ias, icss, - escape.INSERT_ON, inserttext, - escape.INSERT_OFF ] - - if cs == "U": - o.append(escape.IBMPC_OFF) - if whitespace_at_end: - o.append(escape.ERASE_IN_LINE_RIGHT) - - if r.cursor is not None: - x,y = r.cursor - o += [set_cursor_position(x, y), - escape.SHOW_CURSOR ] - self._cy = y - - if self._resized: - # handle resize before trying to draw screen - return - try: - for l in o: - if isinstance(l, bytes) and PYTHON3: - l = l.decode('utf-8') - self.write(l) - self.flush() - except IOError as e: - # ignore interrupted syscall - if e.args[0] != 4: - raise - - self.screen_buf = sb - self._screen_buf_canvas = r - - - def _last_row(self, row): - """On the last row we need to slide the bottom right character - into place. Calculate the new line, attr and an insert sequence - to do that. - - eg. last row: - XXXXXXXXXXXXXXXXXXXXYZ - - Y will be drawn after Z, shifting Z into position. - """ - - new_row = row[:-1] - z_attr, z_cs, last_text = row[-1] - last_cols = util.calc_width(last_text, 0, len(last_text)) - last_offs, z_col = util.calc_text_pos(last_text, 0, - len(last_text), last_cols-1) - if last_offs == 0: - z_text = last_text - del new_row[-1] - # we need another segment - y_attr, y_cs, nlast_text = row[-2] - nlast_cols = util.calc_width(nlast_text, 0, - len(nlast_text)) - z_col += nlast_cols - nlast_offs, y_col = util.calc_text_pos(nlast_text, 0, - len(nlast_text), nlast_cols-1) - y_text = nlast_text[nlast_offs:] - if nlast_offs: - new_row.append((y_attr, y_cs, - nlast_text[:nlast_offs])) - else: - z_text = last_text[last_offs:] - y_attr, y_cs = z_attr, z_cs - nlast_cols = util.calc_width(last_text, 0, - last_offs) - nlast_offs, y_col = util.calc_text_pos(last_text, 0, - last_offs, nlast_cols-1) - y_text = last_text[nlast_offs:last_offs] - if nlast_offs: - new_row.append((y_attr, y_cs, - last_text[:nlast_offs])) - - new_row.append((z_attr, z_cs, z_text)) - return new_row, z_col-y_col, (y_attr, y_cs, y_text) - - - - def clear(self): - """ - Force the screen to be completely repainted on the next - call to draw_screen(). - """ - self.screen_buf = None - self.setup_G1 = True - - - def _attrspec_to_escape(self, a): - """ - Convert AttrSpec instance a to an escape sequence for the terminal - - >>> s = Screen() - >>> s.set_terminal_properties(colors=256) - >>> a2e = s._attrspec_to_escape - >>> a2e(s.AttrSpec('brown', 'dark green')) - '\\x1b[0;33;42m' - >>> a2e(s.AttrSpec('#fea,underline', '#d0d')) - '\\x1b[0;38;5;229;4;48;5;164m' - """ - if a.foreground_high: - fg = "38;5;%d" % a.foreground_number - elif a.foreground_basic: - if a.foreground_number > 7: - if self.fg_bright_is_bold: - fg = "1;%d" % (a.foreground_number - 8 + 30) - else: - fg = "%d" % (a.foreground_number - 8 + 90) - else: - fg = "%d" % (a.foreground_number + 30) - else: - fg = "39" - st = ("1;" * a.bold + "4;" * a.underline + - "5;" * a.blink + "7;" * a.standout) - if a.background_high: - bg = "48;5;%d" % a.background_number - elif a.background_basic: - if a.background_number > 7: - if self.bg_bright_is_blink: - bg = "5;%d" % (a.background_number - 8 + 40) - else: - # this doesn't work on most terminals - bg = "%d" % (a.background_number - 8 + 100) - else: - bg = "%d" % (a.background_number + 40) - else: - bg = "49" - return escape.ESC + "[0;%s;%s%sm" % (fg, st, bg) - - - def set_terminal_properties(self, colors=None, bright_is_bold=None, - has_underline=None): - """ - colors -- number of colors terminal supports (1, 16, 88 or 256) - or None to leave unchanged - bright_is_bold -- set to True if this terminal uses the bold - setting to create bright colors (numbers 8-15), set to False - if this Terminal can create bright colors without bold or - None to leave unchanged - has_underline -- set to True if this terminal can use the - underline setting, False if it cannot or None to leave - unchanged - """ - if colors is None: - colors = self.colors - if bright_is_bold is None: - bright_is_bold = self.fg_bright_is_bold - if has_underline is None: - has_underline = self.has_underline - - if colors == self.colors and bright_is_bold == self.fg_bright_is_bold \ - and has_underline == self.has_underline: - return - - self.colors = colors - self.fg_bright_is_bold = bright_is_bold - self.has_underline = has_underline - - self.clear() - self._pal_escape = {} - for p,v in list(self._palette.items()): - self._on_update_palette_entry(p, *v) - - - - def reset_default_terminal_palette(self): - """ - Attempt to set the terminal palette to default values as taken - from xterm. Uses number of colors from current - set_terminal_properties() screen setting. - """ - if self.colors == 1: - return - - def rgb_values(n): - if self.colors == 16: - aspec = AttrSpec("h%d"%n, "", 256) - else: - aspec = AttrSpec("h%d"%n, "", self.colors) - return aspec.get_rgb_values()[:3] - - entries = [(n,) + rgb_values(n) for n in range(self.colors)] - self.modify_terminal_palette(entries) - - - def modify_terminal_palette(self, entries): - """ - entries - list of (index, red, green, blue) tuples. - - Attempt to set part of the terminal palette (this does not work - on all terminals.) The changes are sent as a single escape - sequence so they should all take effect at the same time. - - 0 <= index < 256 (some terminals will only have 16 or 88 colors) - 0 <= red, green, blue < 256 - """ - - modify = ["%d;rgb:%02x/%02x/%02x" % (index, red, green, blue) - for index, red, green, blue in entries] - self.write("\x1b]4;"+";".join(modify)+"\x1b\\") - self.flush() - - - # shortcut for creating an AttrSpec with this screen object's - # number of colors - AttrSpec = lambda self, fg, bg: AttrSpec(fg, bg, self.colors) - - def _test(): import doctest doctest.testmod() diff --git a/urwid/signals.py b/urwid/signals.py deleted file mode 100644 index 2e92dce..0000000 --- a/urwid/signals.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/python -# -# Urwid signal dispatching -# Copyright (C) 2004-2012 Ian Ward -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Urwid web site: http://excess.org/urwid/ - - -import itertools -import weakref - - -class MetaSignals(type): - """ - register the list of signals in the class varable signals, - including signals in superclasses. - """ - def __init__(cls, name, bases, d): - signals = d.get("signals", []) - for superclass in cls.__bases__: - signals.extend(getattr(superclass, 'signals', [])) - signals = list(dict([(x,None) for x in signals]).keys()) - d["signals"] = signals - register_signal(cls, signals) - super(MetaSignals, cls).__init__(name, bases, d) - -def setdefaultattr(obj, name, value): - # like dict.setdefault() for object attributes - if hasattr(obj, name): - return getattr(obj, name) - setattr(obj, name, value) - return value - -class Key(object): - """ - Minimal class, whose only purpose is to produce objects with a - unique hash - """ - __slots__ = [] - -class Signals(object): - _signal_attr = '_urwid_signals' # attribute to attach to signal senders - - def __init__(self): - self._supported = {} - - def register(self, sig_cls, signals): - """ - :param sig_class: the class of an object that will be sending signals - :type sig_class: class - :param signals: a list of signals that may be sent, typically each - signal is represented by a string - :type signals: signal names - - This function must be called for a class before connecting any - signal callbacks or emiting any signals from that class' objects - """ - self._supported[sig_cls] = signals - - def connect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None): - """ - :param obj: the object sending a signal - :type obj: object - :param name: the signal to listen for, typically a string - :type name: signal name - :param callback: the function to call when that signal is sent - :type callback: function - :param user_arg: deprecated additional argument to callback (appended - after the arguments passed when the signal is - emitted). If None no arguments will be added. - Don't use this argument, use user_args instead. - :param weak_args: additional arguments passed to the callback - (before any arguments passed when the signal - is emitted and before any user_args). - - These arguments are stored as weak references - (but converted back into their original value - before passing them to callback) to prevent - any objects referenced (indirectly) from - weak_args from being kept alive just because - they are referenced by this signal handler. - - Use this argument only as a keyword argument, - since user_arg might be removed in the future. - :type weak_args: iterable - :param user_args: additional arguments to pass to the callback, - (before any arguments passed when the signal - is emitted but after any weak_args). - - Use this argument only as a keyword argument, - since user_arg might be removed in the future. - :type user_args: iterable - - When a matching signal is sent, callback will be called. The - arguments it receives will be the user_args passed at connect - time (as individual arguments) followed by all the positional - parameters sent with the signal. - - As an example of using weak_args, consider the following snippet: - - >>> import urwid - >>> debug = urwid.Text('') - >>> def handler(widget, newtext): - ... debug.set_text("Edit widget changed to %s" % newtext) - >>> edit = urwid.Edit('') - >>> key = urwid.connect_signal(edit, 'change', handler) - - If you now build some interface using "edit" and "debug", the - "debug" widget will show whatever you type in the "edit" widget. - However, if you remove all references to the "debug" widget, it - will still be kept alive by the signal handler. This because the - signal handler is a closure that (implicitly) references the - "edit" widget. If you want to allow the "debug" widget to be - garbage collected, you can create a "fake" or "weak" closure - (it's not really a closure, since it doesn't reference any - outside variables, so it's just a dynamic function): - - >>> debug = urwid.Text('') - >>> def handler(weak_debug, widget, newtext): - ... weak_debug.set_text("Edit widget changed to %s" % newtext) - >>> edit = urwid.Edit('') - >>> key = urwid.connect_signal(edit, 'change', handler, weak_args=[debug]) - - Here the weak_debug parameter in print_debug is the value passed - in the weak_args list to connect_signal. Note that the - weak_debug value passed is not a weak reference anymore, the - signals code transparently dereferences the weakref parameter - before passing it to print_debug. - - Returns a key associated by this signal handler, which can be - used to disconnect the signal later on using - urwid.disconnect_signal_by_key. Alternatively, the signal - handler can also be disconnected by calling - urwid.disconnect_signal, which doesn't need this key. - """ - - sig_cls = obj.__class__ - if not name in self._supported.get(sig_cls, []): - raise NameError("No such signal %r for object %r" % - (name, obj)) - - # Just generate an arbitrary (but unique) key - key = Key() - - signals = setdefaultattr(obj, self._signal_attr, {}) - handlers = signals.setdefault(name, []) - - # Remove the signal handler when any of the weakref'd arguments - # are garbage collected. Note that this means that the handlers - # dictionary can be modified _at any time_, so it should never - # be iterated directly (e.g. iterate only over .keys() and - # .items(), never over .iterkeys(), .iteritems() or the object - # itself). - # We let the callback keep a weakref to the object as well, to - # prevent a circular reference between the handler and the - # object (via the weakrefs, which keep strong references to - # their callbacks) from existing. - obj_weak = weakref.ref(obj) - def weakref_callback(weakref): - o = obj_weak() - if o: - try: - del getattr(o, self._signal_attr, {})[name][key] - except KeyError: - pass - - user_args = self._prepare_user_args(weak_args, user_args, weakref_callback) - handlers.append((key, callback, user_arg, user_args)) - - return key - - def _prepare_user_args(self, weak_args, user_args, callback = None): - # Turn weak_args into weakrefs and prepend them to user_args - return [weakref.ref(a, callback) for a in (weak_args or [])] + (user_args or []) - - - def disconnect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=None): - """ - :param obj: the object to disconnect the signal from - :type obj: object - :param name: the signal to disconnect, typically a string - :type name: signal name - :param callback: the callback function passed to connect_signal - :type callback: function - :param user_arg: the user_arg parameter passed to connect_signal - :param weak_args: the weak_args parameter passed to connect_signal - :param user_args: the weak_args parameter passed to connect_signal - - This function will remove a callback from the list connected - to a signal with connect_signal(). The arguments passed should - be exactly the same as those passed to connect_signal(). - - If the callback is not connected or already disconnected, this - function will simply do nothing. - """ - signals = setdefaultattr(obj, self._signal_attr, {}) - if name not in signals: - return - - handlers = signals[name] - - # Do the same processing as in connect, so we can compare the - # resulting tuple. - user_args = self._prepare_user_args(weak_args, user_args) - - # Remove the given handler - for h in handlers: - if h[1:] == (callback, user_arg, user_args): - return self.disconnect_by_key(obj, name, h[0]) - - def disconnect_by_key(self, obj, name, key): - """ - :param obj: the object to disconnect the signal from - :type obj: object - :param name: the signal to disconnect, typically a string - :type name: signal name - :param key: the key for this signal handler, as returned by - connect_signal(). - :type key: Key - - This function will remove a callback from the list connected - to a signal with connect_signal(). The key passed should be the - value returned by connect_signal(). - - If the callback is not connected or already disconnected, this - function will simply do nothing. - """ - signals = setdefaultattr(obj, self._signal_attr, {}) - handlers = signals.get(name, []) - handlers[:] = [h for h in handlers if h[0] is not key] - - def emit(self, obj, name, *args): - """ - :param obj: the object sending a signal - :type obj: object - :param name: the signal to send, typically a string - :type name: signal name - :param \*args: zero or more positional arguments to pass to the signal - callback functions - - This function calls each of the callbacks connected to this signal - with the args arguments as positional parameters. - - This function returns True if any of the callbacks returned True. - """ - result = False - signals = getattr(obj, self._signal_attr, {}) - handlers = signals.get(name, []) - for key, callback, user_arg, user_args in handlers: - result |= self._call_callback(callback, user_arg, user_args, args) - return result - - def _call_callback(self, callback, user_arg, user_args, emit_args): - if user_args: - args_to_pass = [] - for arg in user_args: - if isinstance(arg, weakref.ReferenceType): - arg = arg() - if arg is None: - # If the weakref is None, the referenced object - # was cleaned up. We just skip the entire - # callback in this case. The weakref cleanup - # handler will have removed the callback when - # this happens, so no need to actually remove - # the callback here. - return False - args_to_pass.append(arg) - - args_to_pass.extend(emit_args) - else: - # Optimization: Don't create a new list when there are - # no user_args - args_to_pass = emit_args - - # The deprecated user_arg argument was added to the end - # instead of the beginning. - if user_arg is not None: - args_to_pass = itertools.chain(args_to_pass, (user_arg,)) - - return bool(callback(*args_to_pass)) - -_signals = Signals() -emit_signal = _signals.emit -register_signal = _signals.register -connect_signal = _signals.connect -disconnect_signal = _signals.disconnect -disconnect_signal_by_key = _signals.disconnect_by_key -