You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
12 KiB
Python
456 lines
12 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Urwid utility functions
|
|
# Copyright (C) 2004-2007 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/
|
|
|
|
from __future__ import nested_scopes
|
|
|
|
import escape
|
|
|
|
import encodings
|
|
|
|
try:
|
|
import str_util
|
|
except ImportError:
|
|
import old_str_util as str_util
|
|
|
|
# bring str_util functions into our namespace
|
|
calc_text_pos = str_util.calc_text_pos
|
|
calc_width = str_util.calc_width
|
|
is_wide_char = str_util.is_wide_char
|
|
move_next_char = str_util.move_next_char
|
|
move_prev_char = str_util.move_prev_char
|
|
within_double_byte = str_util.within_double_byte
|
|
|
|
|
|
try: enumerate
|
|
except: enumerate = lambda x: zip(range(len(x)),x) # old python
|
|
|
|
|
|
# Try to determine if using a supported double-byte encoding
|
|
import locale
|
|
try:
|
|
try:
|
|
locale.setlocale( locale.LC_ALL, "" )
|
|
except locale.Error:
|
|
pass
|
|
detected_encoding = locale.getlocale()[1]
|
|
if not detected_encoding:
|
|
detected_encoding = ""
|
|
except ValueError, e:
|
|
# with invalid LANG value python will throw ValueError
|
|
if e.args and e.args[0].startswith("unknown locale"):
|
|
detected_encoding = ""
|
|
else:
|
|
raise
|
|
|
|
_target_encoding = None
|
|
_use_dec_special = True
|
|
|
|
|
|
def set_encoding( encoding ):
|
|
"""
|
|
Set the byte encoding to assume when processing strings and the
|
|
encoding to use when converting unicode strings.
|
|
"""
|
|
encoding = encoding.lower()
|
|
|
|
global _target_encoding, _use_dec_special
|
|
|
|
if encoding in ( 'utf-8', 'utf8', 'utf' ):
|
|
str_util.set_byte_encoding("utf8")
|
|
|
|
_use_dec_special = False
|
|
elif encoding in ( 'euc-jp' # JISX 0208 only
|
|
, 'euc-kr', 'euc-cn', 'euc-tw' # CNS 11643 plain 1 only
|
|
, 'gb2312', 'gbk', 'big5', 'cn-gb', 'uhc'
|
|
# these shouldn't happen, should they?
|
|
, 'eucjp', 'euckr', 'euccn', 'euctw', 'cncb' ):
|
|
str_util.set_byte_encoding("wide")
|
|
|
|
_use_dec_special = True
|
|
else:
|
|
str_util.set_byte_encoding("narrow")
|
|
_use_dec_special = True
|
|
|
|
# if encoding is valid for conversion from unicode, remember it
|
|
_target_encoding = 'ascii'
|
|
try:
|
|
if encoding:
|
|
u"".encode(encoding)
|
|
_target_encoding = encoding
|
|
except LookupError: pass
|
|
|
|
|
|
def get_encoding_mode():
|
|
"""
|
|
Get the mode Urwid is using when processing text strings.
|
|
Returns 'narrow' for 8-bit encodings, 'wide' for CJK encodings
|
|
or 'utf8' for UTF-8 encodings.
|
|
"""
|
|
return str_util.get_byte_encoding()
|
|
|
|
|
|
def apply_target_encoding( s ):
|
|
"""
|
|
Return (encoded byte string, character set rle).
|
|
"""
|
|
if _use_dec_special and type(s) == type(u""):
|
|
# first convert drawing characters
|
|
try:
|
|
s = s.translate( escape.DEC_SPECIAL_CHARMAP )
|
|
except NotImplementedError:
|
|
# python < 2.4 needs to do this the hard way..
|
|
for c, alt in zip(escape.DEC_SPECIAL_CHARS,
|
|
escape.ALT_DEC_SPECIAL_CHARS):
|
|
s = s.replace( c, escape.SO+alt+escape.SI )
|
|
|
|
if type(s) == type(u""):
|
|
s = s.replace( escape.SI+escape.SO, u"" ) # remove redundant shifts
|
|
s = s.encode( _target_encoding )
|
|
|
|
sis = s.split( escape.SO )
|
|
|
|
sis0 = sis[0].replace( escape.SI, "" )
|
|
sout = []
|
|
cout = []
|
|
if sis0:
|
|
sout.append( sis0 )
|
|
cout.append( (None,len(sis0)) )
|
|
|
|
if len(sis)==1:
|
|
return sis0, cout
|
|
|
|
for sn in sis[1:]:
|
|
sl = sn.split( escape.SI, 1 )
|
|
if len(sl) == 1:
|
|
sin = sl[0]
|
|
sout.append(sin)
|
|
rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
|
|
continue
|
|
sin, son = sl
|
|
son = son.replace( escape.SI, "" )
|
|
if sin:
|
|
sout.append(sin)
|
|
rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
|
|
if son:
|
|
sout.append(son)
|
|
rle_append_modify(cout, (None, len(son)))
|
|
|
|
return "".join(sout), cout
|
|
|
|
|
|
######################################################################
|
|
# Try to set the encoding using the one detected by the locale module
|
|
set_encoding( detected_encoding )
|
|
######################################################################
|
|
|
|
|
|
def supports_unicode():
|
|
"""
|
|
Return True if python is able to convert non-ascii unicode strings
|
|
to the current encoding.
|
|
"""
|
|
return _target_encoding and _target_encoding != 'ascii'
|
|
|
|
|
|
|
|
|
|
|
|
def calc_trim_text( text, start_offs, end_offs, start_col, end_col ):
|
|
"""
|
|
Calculate the result of trimming text.
|
|
start_offs -- offset into text to treat as screen column 0
|
|
end_offs -- offset into text to treat as the end of the line
|
|
start_col -- screen column to trim at the left
|
|
end_col -- screen column to trim at the right
|
|
|
|
Returns (start, end, pad_left, pad_right), where:
|
|
start -- resulting start offset
|
|
end -- resulting end offset
|
|
pad_left -- 0 for no pad or 1 for one space to be added
|
|
pad_right -- 0 for no pad or 1 for one space to be added
|
|
"""
|
|
l = []
|
|
spos = start_offs
|
|
pad_left = pad_right = 0
|
|
if start_col > 0:
|
|
spos, sc = calc_text_pos( text, spos, end_offs, start_col )
|
|
if sc < start_col:
|
|
pad_left = 1
|
|
spos, sc = calc_text_pos( text, start_offs,
|
|
end_offs, start_col+1 )
|
|
run = end_col - start_col - pad_left
|
|
pos, sc = calc_text_pos( text, spos, end_offs, run )
|
|
if sc < run:
|
|
pad_right = 1
|
|
return ( spos, pos, pad_left, pad_right )
|
|
|
|
|
|
|
|
|
|
def trim_text_attr_cs( text, attr, cs, start_col, end_col ):
|
|
"""
|
|
Return ( trimmed text, trimmed attr, trimmed cs ).
|
|
"""
|
|
spos, epos, pad_left, pad_right = calc_trim_text(
|
|
text, 0, len(text), start_col, end_col )
|
|
attrtr = rle_subseg( attr, spos, epos )
|
|
cstr = rle_subseg( cs, spos, epos )
|
|
if pad_left:
|
|
al = rle_get_at( attr, spos-1 )
|
|
rle_append_beginning_modify( attrtr, (al, 1) )
|
|
rle_append_beginning_modify( cstr, (None, 1) )
|
|
if pad_right:
|
|
al = rle_get_at( attr, epos )
|
|
rle_append_modify( attrtr, (al, 1) )
|
|
rle_append_modify( cstr, (None, 1) )
|
|
|
|
return " "*pad_left + text[spos:epos] + " "*pad_right, attrtr, cstr
|
|
|
|
|
|
def rle_get_at( rle, pos ):
|
|
"""
|
|
Return the attribute at offset pos.
|
|
"""
|
|
x = 0
|
|
if pos < 0:
|
|
return None
|
|
for a, run in rle:
|
|
if x+run > pos:
|
|
return a
|
|
x += run
|
|
return None
|
|
|
|
|
|
def rle_subseg( rle, start, end ):
|
|
"""Return a sub segment of an rle list."""
|
|
l = []
|
|
x = 0
|
|
for a, run in rle:
|
|
if start:
|
|
if start >= run:
|
|
start -= run
|
|
x += run
|
|
continue
|
|
x += start
|
|
run -= start
|
|
start = 0
|
|
if x >= end:
|
|
break
|
|
if x+run > end:
|
|
run = end-x
|
|
x += run
|
|
l.append( (a, run) )
|
|
return l
|
|
|
|
|
|
def rle_len( rle ):
|
|
"""
|
|
Return the number of characters covered by a run length
|
|
encoded attribute list.
|
|
"""
|
|
|
|
run = 0
|
|
for v in rle:
|
|
assert type(v) == type(()), `rle`
|
|
a, r = v
|
|
run += r
|
|
return run
|
|
|
|
def rle_append_beginning_modify( rle, (a, r) ):
|
|
"""
|
|
Append (a, r) to BEGINNING of rle.
|
|
Merge with first run when possible
|
|
|
|
MODIFIES rle parameter contents. Returns None.
|
|
"""
|
|
if not rle:
|
|
rle[:] = [(a, r)]
|
|
else:
|
|
al, run = rle[0]
|
|
if a == al:
|
|
rle[0] = (a,run+r)
|
|
else:
|
|
rle[0:0] = [(al, r)]
|
|
|
|
|
|
def rle_append_modify( rle, (a, r) ):
|
|
"""
|
|
Append (a,r) to the rle list rle.
|
|
Merge with last run when possible.
|
|
|
|
MODIFIES rle parameter contents. Returns None.
|
|
"""
|
|
if not rle or rle[-1][0] != a:
|
|
rle.append( (a,r) )
|
|
return
|
|
la,lr = rle[-1]
|
|
rle[-1] = (a, lr+r)
|
|
|
|
def rle_join_modify( rle, rle2 ):
|
|
"""
|
|
Append attribute list rle2 to rle.
|
|
Merge last run of rle with first run of rle2 when possible.
|
|
|
|
MODIFIES attr parameter contents. Returns None.
|
|
"""
|
|
if not rle2:
|
|
return
|
|
rle_append_modify(rle, rle2[0])
|
|
rle += rle2[1:]
|
|
|
|
def rle_product( rle1, rle2 ):
|
|
"""
|
|
Merge the runs of rle1 and rle2 like this:
|
|
eg.
|
|
rle1 = [ ("a", 10), ("b", 5) ]
|
|
rle2 = [ ("Q", 5), ("P", 10) ]
|
|
rle_product: [ (("a","Q"), 5), (("a","P"), 5), (("b","P"), 5) ]
|
|
|
|
rle1 and rle2 are assumed to cover the same total run.
|
|
"""
|
|
i1 = i2 = 1 # rle1, rle2 indexes
|
|
if not rle1 or not rle2: return []
|
|
a1, r1 = rle1[0]
|
|
a2, r2 = rle2[0]
|
|
|
|
l = []
|
|
while r1 and r2:
|
|
r = min(r1, r2)
|
|
rle_append_modify( l, ((a1,a2),r) )
|
|
r1 -= r
|
|
if r1 == 0 and i1< len(rle1):
|
|
a1, r1 = rle1[i1]
|
|
i1 += 1
|
|
r2 -= r
|
|
if r2 == 0 and i2< len(rle2):
|
|
a2, r2 = rle2[i2]
|
|
i2 += 1
|
|
return l
|
|
|
|
|
|
def rle_factor( rle ):
|
|
"""
|
|
Inverse of rle_product.
|
|
"""
|
|
rle1 = []
|
|
rle2 = []
|
|
for (a1, a2), r in rle:
|
|
rle_append_modify( rle1, (a1, r) )
|
|
rle_append_modify( rle2, (a2, r) )
|
|
return rle1, rle2
|
|
|
|
|
|
class TagMarkupException( Exception ): pass
|
|
|
|
def decompose_tagmarkup( tm ):
|
|
"""Return (text string, attribute list) for tagmarkup passed."""
|
|
|
|
tl, al = _tagmarkup_recurse( tm, None )
|
|
text = "".join(tl)
|
|
|
|
if al and al[-1][0] is None:
|
|
del al[-1]
|
|
|
|
return text, al
|
|
|
|
def _tagmarkup_recurse( tm, attr ):
|
|
"""Return (text list, attribute list) for tagmarkup passed.
|
|
|
|
tm -- tagmarkup
|
|
attr -- current attribute or None"""
|
|
|
|
if type(tm) == list:
|
|
# for lists recurse to process each subelement
|
|
rtl = []
|
|
ral = []
|
|
for element in tm:
|
|
tl, al = _tagmarkup_recurse( element, attr )
|
|
if ral:
|
|
# merge attributes when possible
|
|
last_attr, last_run = ral[-1]
|
|
top_attr, top_run = al[0]
|
|
if last_attr == top_attr:
|
|
ral[-1] = (top_attr, last_run + top_run)
|
|
del al[-1]
|
|
rtl += tl
|
|
ral += al
|
|
return rtl, ral
|
|
|
|
if type(tm) == type(()):
|
|
# tuples mark a new attribute boundary
|
|
if len(tm) != 2:
|
|
raise TagMarkupException, "Tuples must be in the form (attribute, tagmarkup): %s" % `tm`
|
|
|
|
attr, element = tm
|
|
return _tagmarkup_recurse( element, attr )
|
|
|
|
if type(tm) not in (str, unicode):
|
|
# last ditch, try converting the object to unicode
|
|
try:
|
|
tm = uncode(tm)
|
|
except:
|
|
raise TagMarkupException, "Invalid markup element: %r" % tm
|
|
|
|
# text
|
|
return [tm], [(attr, len(tm))]
|
|
|
|
|
|
|
|
def is_mouse_event( ev ):
|
|
return type(ev) == type(()) and len(ev)==4 and ev[0].find("mouse")>=0
|
|
|
|
def is_mouse_press( ev ):
|
|
return ev.find("press")>=0
|
|
|
|
|
|
|
|
class MetaSuper(type):
|
|
"""adding .__super"""
|
|
def __init__(cls, name, bases, d):
|
|
super(MetaSuper, cls).__init__(name, bases, d)
|
|
if hasattr(cls, "_%s__super" % name):
|
|
raise AttributeError, "Class has same name as one of its super classes"
|
|
setattr(cls, "_%s__super" % name, super(cls))
|
|
|
|
|
|
|
|
def int_scale(val, val_range, out_range):
|
|
"""
|
|
Scale val in the range [0, val_range-1] to an integer in the range
|
|
[0, out_range-1]. This implementaton uses the "round-half-up" rounding
|
|
method.
|
|
|
|
>>> "%x" % int_scale(0x7, 0x10, 0x10000)
|
|
'7777'
|
|
>>> "%x" % int_scale(0x5f, 0x100, 0x10)
|
|
'6'
|
|
>>> int_scale(2, 6, 101)
|
|
40
|
|
>>> int_scale(1, 3, 4)
|
|
2
|
|
"""
|
|
num = int(val * (out_range-1) * 2 + (val_range-1))
|
|
dem = ((val_range-1) * 2)
|
|
# if num % dem == 0 then we are exactly half-way and have rounded up.
|
|
return num / dem
|
|
|