Started paring down urwid.

This commit is contained in:
Andrew Hamilton 2016-12-03 13:24:41 +01:00
parent d8f1f211d1
commit 23a0fe2ab4
5 changed files with 13 additions and 1755 deletions

View file

@ -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()

View file

@ -56,12 +56,6 @@ 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

View file

@ -20,639 +20,9 @@
import os
import sys
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

View file

@ -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()

View file

@ -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