def calc_line_pos( text, line_layout, pref_col ): """ Calculate the closest linear position to pref_col given a line layout structure. Returns None if no position found. """ closest_sc = None closest_pos = None current_sc = 0 if pref_col == 'left': for seg in line_layout: s = LayoutSegment(seg) if s.offs is not None: return s.offs return elif pref_col == 'right': for seg in line_layout: s = LayoutSegment(seg) if s.offs is not None: closest_pos = s s = closest_pos if s is None: return if s.end is None: return s.offs return calc_text_pos( text, s.offs, s.end, s.sc-1)[0] for seg in line_layout: s = LayoutSegment(seg) if s.offs is not None: if s.end is not None: if (current_sc <= pref_col and pref_col < current_sc + s.sc): # exact match within this segment return calc_text_pos( text, s.offs, s.end, pref_col - current_sc )[0] elif current_sc <= pref_col: closest_sc = current_sc + s.sc - 1 closest_pos = s if closest_sc is None or ( abs(pref_col-current_sc) < abs(pref_col-closest_sc) ): # this screen column is closer closest_sc = current_sc closest_pos = s.offs if current_sc > closest_sc: # we're moving past break current_sc += s.sc if closest_pos is None or type(closest_pos) == int: return closest_pos # return the last positions in the segment "closest_pos" s = closest_pos return calc_text_pos( text, s.offs, s.end, s.sc-1)[0]
def html_span(s, aspec, cursor=-1): fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values() # use real colours instead of default fg/bg if fg_r is None: fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b if bg_r is None: bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b) html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b) if aspec.standout: html_fg, html_bg = html_bg, html_fg extra = (";text-decoration:underline" * aspec.underline + ";font-weight:bold" * aspec.bold) def html_span(fg, bg, s): if not s: return "" return ('<span style="color:%s;' 'background:%s%s">%s</span>' % (fg, bg, extra, html_escape(s))) if cursor >= 0: c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) c2_off = util.move_next_char(s, c_off, len(s)) return (html_span(html_fg, html_bg, s[:c_off]) + html_span(html_bg, html_fg, s[c_off:c2_off]) + html_span(html_fg, html_bg, s[c2_off:])) else: return html_span(html_fg, html_bg, s)
def html_span(s, aspec, cursor = -1): fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values() # use real colours instead of default fg/bg if fg_r is None: fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b if bg_r is None: bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b) html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b) if aspec.standout: html_fg, html_bg = html_bg, html_fg extra = (";text-decoration:underline" * aspec.underline + ";font-weight:bold" * aspec.bold) def html_span(fg, bg, s): if not s: return "" return ('<span style="color:%s;' 'background:%s%s">%s</span>' % (fg, bg, extra, html_escape(s))) if cursor >= 0: c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) c2_off = util.move_next_char(s, c_off, len(s)) return (html_span(html_fg, html_bg, s[:c_off]) + html_span(html_bg, html_fg, s[c_off:c2_off]) + html_span(html_fg, html_bg, s[c2_off:])) else: return html_span(html_fg, html_bg, s)
def __init__(self, fill_char, cols, rows): Canvas.__init__(self) end, col = calc_text_pos(fill_char, 0, len(fill_char), 1) assert col == 1, "Invalid fill_char: %r" % fill_char self._text, cs = apply_target_encoding(fill_char[:end]) self._cs = cs[0][0] self.size = cols, rows self.cursor = None
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 code_span(s, fg, bg, cursor=-1): code_fg = _code_colours[fg] code_bg = _code_colours[bg] if cursor >= 0: c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) c2_off = util.move_next_char(s, c_off, len(s)) return (code_fg + code_bg + s[:c_off] + "\n" + code_bg + code_fg + s[c_off:c2_off] + "\n" + code_fg + code_bg + s[c2_off:] + "\n") else: return code_fg + code_bg + s + "\n"
def code_span( s, fg, bg, cursor = -1): code_fg = _code_colours[ fg ] code_bg = _code_colours[ bg ] if cursor >= 0: c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor) c2_off = util.move_next_char(s, c_off, len(s)) return ( code_fg + code_bg + s[:c_off] + "\n" + code_bg + code_fg + s[c_off:c2_off] + "\n" + code_fg + code_bg + s[c2_off:] + "\n") else: return code_fg + code_bg + s + "\n"
def make_canvas(txt, attr, maxcol, fill_attr=None): processed_txt = [] processed_attr = [] processed_cs = [] for line, line_attr in zip(txt, attr): # filter out zero-length attrs line_attr = [(aname, l) for aname, l in line_attr if l > 0] diff = maxcol - text_width(line) if diff > 0: line += " " * diff line_attr.append((fill_attr, diff)) else: from urwid.util import rle_subseg line = line[:calc_text_pos(line, 0, len(line), maxcol)[0]] line_attr = rle_subseg(line_attr, 0, maxcol) from urwid.util import apply_target_encoding encoded_line, line_cs = apply_target_encoding(line) # line_cs contains byte counts as requested by TextCanvas, but # line_attr still contains column counts at this point: let's fix this. def get_byte_line_attr(line, line_attr): i = 0 for label, column_count in line_attr: byte_count = len(line[i:i + column_count].encode(_target_encoding)) i += column_count yield label, byte_count line_attr = list(get_byte_line_attr(line, line_attr)) processed_txt.append(encoded_line) processed_attr.append(line_attr) processed_cs.append(line_cs) return urwid.TextCanvas(processed_txt, processed_attr, processed_cs, maxcol=maxcol)
def make_canvas(txt, attr, maxcol, fill_attr=None): processed_txt = [] processed_attr = [] processed_cs = [] for line, line_attr in zip(txt, attr): # filter out zero-length attrs line_attr = [(aname, l) for aname, l in line_attr if l > 0] diff = maxcol - text_width(line) if diff > 0: line += " "*diff line_attr.append((fill_attr, diff)) else: from urwid.util import rle_subseg line = line[:calc_text_pos(line, 0, len(line), maxcol)[0]] line_attr = rle_subseg(line_attr, 0, maxcol) from urwid.util import apply_target_encoding encoded_line, line_cs = apply_target_encoding(line) # line_cs contains byte counts as requested by TextCanvas, but # line_attr still contains column counts at this point: let's fix this. def get_byte_line_attr(line, line_attr): i = 0 for label, column_count in line_attr: byte_count = len(line[i:i+column_count].encode(_target_encoding)) i += column_count yield label, byte_count line_attr = list(get_byte_line_attr(line, line_attr)) processed_txt.append(encoded_line) processed_attr.append(line_attr) processed_cs.append(line_cs) return urwid.TextCanvas( processed_txt, processed_attr, processed_cs, maxcol=maxcol)
def ctptest(self, text, tests): text = B(text) for s,e,p, expected in tests: got = util.calc_text_pos( text, s, e, p ) assert got == expected, "%r got:%r expected:%r" % ((s,e,p), got, expected)
def calculate_text_segments(self, text, width, wrap): """ Calculate the segments of text to display given width screen columns to display them. text - unicode text or byte string to display width - number of available screen columns wrap - wrapping mode used Returns a layout structure without alignment applied. """ nl, nl_o, sp_o = "\n", "\n", " " if PYTHON3 and isinstance(text, bytes): nl = B(nl) # can only find bytes in python3 bytestrings nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value sp_o = ord(sp_o) b = [] p = 0 if wrap == 'clip': # no wrapping to calculate, so it's easy. while p<=len(text): n_cr = text.find(nl, p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) l = [(0,n_cr)] if p!=n_cr: l = [(sc, p, n_cr)] + l b.append(l) p = n_cr+1 return b while p<=len(text): # look for next eligible line break n_cr = text.find(nl, p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) if sc == 0: # removed character hint b.append([(0,n_cr)]) p = n_cr+1 continue if sc <= width: # this segment fits b.append([(sc,p,n_cr), # removed character hint (0,n_cr)]) p = n_cr+1 continue pos, sc = calc_text_pos( text, p, n_cr, width ) if pos == p: # pathological width=1 double-byte case raise CanNotDisplayText( "Wide character will not fit in 1-column width") if wrap == 'any': b.append([(sc,p,pos)]) p = pos continue assert wrap == 'space' if text[pos] == sp_o: # perfect space wrap b.append([(sc,p,pos), # removed character hint (0,pos)]) p = pos+1 continue if is_wide_char(text, pos): # perfect next wide b.append([(sc,p,pos)]) p = pos continue prev = pos while prev > p: prev = move_prev_char(text, p, prev) if text[prev] == sp_o: sc = calc_width(text,p,prev) l = [(0,prev)] if p!=prev: l = [(sc,p,prev)] + l b.append(l) p = prev+1 break if is_wide_char(text,prev): # wrap after wide char next = move_next_char(text, prev, pos) sc = calc_width(text,p,next) b.append([(sc,p,next)]) p = next break else: # unwrap previous line space if possible to # fit more text (we're breaking a word anyway) if b and (len(b[-1]) == 2 or ( len(b[-1])==1 and len(b[-1][0])==2 )): # look for removed space above if len(b[-1]) == 1: [(h_sc, h_off)] = b[-1] p_sc = 0 p_off = p_end = h_off else: [(p_sc, p_off, p_end), (h_sc, h_off)] = b[-1] if (p_sc < width and h_sc==0 and text[h_off] == sp_o): # combine with previous line del b[-1] p = p_off pos, sc = calc_text_pos( text, p, n_cr, width ) b.append([(sc,p,pos)]) # check for trailing " " or "\n" p = pos if p < len(text) and ( text[p] in (sp_o, nl_o)): # removed character hint b[-1].append((0,p)) p += 1 continue # force any char wrap b.append([(sc,p,pos)]) p = pos return b
def ctptest(self, text, tests): text = B(text) for s, e, p, expected in tests: got = util.calc_text_pos(text, s, e, p) assert got == expected, "%r got:%r expected:%r" % ( (s, e, p), got, expected)
def calculate_text_segments(self, text, width, wrap): """ Calculate the segments of text to display given width screen columns to display them. text - unicode text or byte string to display width - number of available screen columns wrap - wrapping mode used Returns a layout structure without aligmnent applied. """ nl, nl_o, sp_o = "\n", "\n", " " if PYTHON3 and isinstance(text, bytes): nl = B(nl) # can only find bytes in python3 bytestrings nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value sp_o = ord(sp_o) b = [] p = 0 if wrap == 'clip': # no wrapping to calculate, so it's easy. while p <= len(text): n_cr = text.find(nl, p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) l = [(0, n_cr)] if p != n_cr: l = [(sc, p, n_cr)] + l b.append(l) p = n_cr + 1 return b while p <= len(text): # look for next eligible line break n_cr = text.find(nl, p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) if sc == 0: # removed character hint b.append([(0, n_cr)]) p = n_cr + 1 continue if sc <= width: # this segment fits b.append([ (sc, p, n_cr), # removed character hint (0, n_cr) ]) p = n_cr + 1 continue pos, sc = calc_text_pos(text, p, n_cr, width) if pos == p: # pathological width=1 double-byte case raise CanNotDisplayText( "Wide character will not fit in 1-column width") if wrap == 'any': b.append([(sc, p, pos)]) p = pos continue assert wrap == 'space' if text[pos] == sp_o: # perfect space wrap b.append([ (sc, p, pos), # removed character hint (0, pos) ]) p = pos + 1 continue if is_wide_char(text, pos): # perfect next wide b.append([(sc, p, pos)]) p = pos continue prev = pos while prev > p: prev = move_prev_char(text, p, prev) if text[prev] == sp_o: sc = calc_width(text, p, prev) l = [(0, prev)] if p != prev: l = [(sc, p, prev)] + l b.append(l) p = prev + 1 break if is_wide_char(text, prev): # wrap after wide char next = move_next_char(text, prev, pos) sc = calc_width(text, p, next) b.append([(sc, p, next)]) p = next break else: # unwrap previous line space if possible to # fit more text (we're breaking a word anyway) if b and (len(b[-1]) == 2 or (len(b[-1]) == 1 and len(b[-1][0]) == 2)): # look for removed space above if len(b[-1]) == 1: [(h_sc, h_off)] = b[-1] p_sc = 0 p_off = p_end = h_off else: [(p_sc, p_off, p_end), (h_sc, h_off)] = b[-1] if (p_sc < width and h_sc == 0 and text[h_off] == sp_o): # combine with previous line del b[-1] p = p_off pos, sc = calc_text_pos(text, p, n_cr, width) b.append([(sc, p, pos)]) # check for trailing " " or "\n" p = pos if p < len(text) and (text[p] in (sp_o, nl_o)): # removed character hint b[-1].append((0, p)) p += 1 continue # force any char wrap b.append([(sc, p, pos)]) p = pos return b
def calculate_text_segments( self, text, width, wrap ): """ Calculate the segments of text to display given width screen columns to display them. text - text to display width - number of available screen columns wrap - wrapping mode used Returns a layout structure without aligmnent applied. """ b = [] p = 0 if wrap == 'clip': # no wrapping to calculate, so it's easy. while p<=len(text): n_cr = text.find("\n", p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) l = [(0,n_cr)] if p!=n_cr: l = [(sc, p, n_cr)] + l b.append(l) p = n_cr+1 return b while p<=len(text): # look for next eligible line break n_cr = text.find("\n", p) if n_cr == -1: n_cr = len(text) sc = calc_width(text, p, n_cr) if sc == 0: # removed character hint b.append([(0,n_cr)]) p = n_cr+1 continue if sc <= width: # this segment fits b.append([(sc,p,n_cr), # removed character hint (0,n_cr)]) p = n_cr+1 continue pos, sc = calc_text_pos( text, p, n_cr, width ) # FIXME: handle pathological width=1 double-byte case if wrap == 'any': b.append([(sc,p,pos)]) p = pos continue assert wrap == 'space' if text[pos] == " ": # perfect space wrap b.append([(sc,p,pos), # removed character hint (0,pos)]) p = pos+1 continue if is_wide_char(text, pos): # perfect next wide b.append([(sc,p,pos)]) p = pos continue prev = pos while prev > p: prev = move_prev_char(text, p, prev) if text[prev] == " ": sc = calc_width(text,p,prev) l = [(0,prev)] if p!=prev: l = [(sc,p,prev)] + l b.append(l) p = prev+1 break if is_wide_char(text,prev): # wrap after wide char next = move_next_char(text, prev, pos) sc = calc_width(text,p,next) b.append([(sc,p,next)]) p = next break else: # unwrap previous line space if possible to # fit more text (we're breaking a word anyway) if b and (len(b[-1]) == 2 or ( len(b[-1])==1 and len(b[-1][0])==2 )): # look for removed space above if len(b[-1]) == 1: [(h_sc, h_off)] = b[-1] p_sc = 0 p_off = p_end = h_off else: [(p_sc, p_off, p_end), (h_sc, h_off)] = b[-1] if (p_sc < width and h_sc==0 and text[h_off] == " "): # combine with previous line del b[-1] p = p_off pos, sc = calc_text_pos( text, p, n_cr, width ) b.append([(sc,p,pos)]) # check for trailing " " or "\n" p = pos if p < len(text) and ( text[p] in (" ","\n")): # removed character hint b[-1].append((0,p)) p += 1 continue # force any char wrap b.append([(sc,p,pos)]) p = pos return b
def calculate_text_segments(self, text, width, wrap): """ Calculate the segments of text to display given width screen columns to display them. text - unicode text or byte string to display width - number of available screen columns wrap - wrapping mode used Returns a layout structure without alignment applied. """ # TODO: This function is a horror and a mess, and really hard to # understand. It's based on urwids StandardLayout, which by itself # is overly complex, and I added tab handling, which made it worse. # It's a prime candidate for refacturing, making easier to understand # and as it is heavily used, profiling would be nice too. nl, nl_o, sp_o, tab_o = "\n", "\n", " ", "\t" if PYTHON3 and isinstance(text, bytes): nl = B(nl) # can only find bytes in python3 bytestrings nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value sp_o = ord(sp_o) tab_o = ord(tab_o) b = [] p = 0 if wrap == 'clip': # no wrapping to calculate, so it's easy. l = [] while p <= len(text): n_cr = find_newline(text, p) if p != n_cr: line = text[p:n_cr] pt = 0 while pt < len(line): n_tab = line.find(tab_o, pt) if n_tab == -1: end = len(line) else: end = n_tab sc = calc_width(line, pt, end) if sc != 0: l.append((sc, p + pt, p + end)) if end == n_tab: # A tab was found extra_space = (self.tab_width - ( sc % self.tab_width)) l.append((extra_space, p + n_tab)) pt = end + 1 l.append((0, n_cr)) b.append(l) l = [] if text[n_cr:n_cr+2] in TWOCHAR_NEWLINES: # Two char newline: p = n_cr + 2 else: p = n_cr + 1 return b while p <= len(text): # look for next eligible line break n_cr = find_newline(text, p) line = text[p:n_cr] l = [] pt = 0 lc = 0 while pt < len(line): n_tab = line.find(tab_o, pt) if n_tab == -1: end = len(line) else: end = n_tab sc = calc_width(line, pt, end) if lc + sc <= width: # this segment fits if sc: l.append((sc, p + pt, p + end)) if end == n_tab: # A tab was found extra_space = self.tab_width - (sc % self.tab_width) l.append((extra_space, p + n_tab)) lc += extra_space else: # removed character hint l.append((0, p + end)) pt = end + 1 lc += sc if lc >= width: # The tab can sometimes push line length to width, and # then we adjust the line length and make a new line. overshoot = lc - width spaces, pos = l[-1] l[-1] = (spaces - overshoot, pos) b.append(l) l = [] lc = 0 continue # This segment does not fit. Let's fit it. pos, sc = calc_text_pos(line, pt, end, width - lc) if pos == pt: # pathological width=1 double-byte case raise CanNotDisplayText( "Wide character will not fit in 1-column width") if wrap == 'any': l.append((sc, p + pt, p + pos)) l.append((0, p + pos)) b.append(l) l = [] lc = 0 pt = pos continue assert wrap == 'space' if line[pos] == sp_o: # perfect space wrap l.append((sc, p + pt, p + pos)) # removed character hint l.append((0, p + pos)) b.append(l) l = [] lc = 0 pt = pos + 1 continue if is_wide_char(line, pos): # perfect next wide l.append((sc, p + pt, p + pos)) b.append(l) l = [] lc = 0 pt = pos continue prev = pos while prev > pt: prev = move_prev_char(line, pt, prev) if line[prev] == sp_o: sc = calc_width(line, pt, prev) if prev != pt: l.append((sc, p + pt, p + prev)) l.append((0, p + prev)) b.append(l) l = [] lc = 0 pt = prev + 1 break if is_wide_char(line, prev): # wrap after wide char nextc = move_next_char(line, prev, pos) sc = calc_width(line, pt, nextc) l.append((sc, p + pt, p + nextc)) b.append(l) l = [] lc = 0 pt = nextc break else: if lc == 0: # unwrap previous line space if possible to # fit more text (we're breaking a word anyway) if b and (len(b[-1]) == 2 or (len(b[-1]) == 1 and len(b[-1][0]) == 2)): # look for removed space above if len(b[-1]) == 1: [(h_sc, h_off)] = b[-1] p_sc = 0 p_off = p_end = h_off else: [(p_sc, p_off, p_end), (h_sc, h_off)] = b[-1][-2:] if (p_sc < width and h_sc == 0 and text[h_off] == sp_o): # combine with previous line old_line = b[-1][:-2] del b[-1] pt = p_off - p pos, sc = calc_text_pos( line, pt, end, width) old_line.append((sc, p + pt, p + pos)) b.append(old_line) # check for trailing " " or "\n" pt = pos if pt < len(text) and ( text[pt] in (sp_o, nl_o)): # removed character hint b[-1].append((0, p + pt)) pt += 1 continue # Break on previous tab, and try again. if l: b.append(l) l = [] lc = 0 continue # There is no space to break the line on, unwrapping the # previous line doesn't help, I guess we just break on a # character. b.append([(sc, p + pt, p + pos)]) l = [] lc = 0 pt = pos # force any char wrap if l: b.append(l) elif not line: # An empty line. b.append([(0, n_cr)]) pt = 1 if text[pt-1:pt+1] in TWOCHAR_NEWLINES: # Two char newline: pt += 1 p += pt return b