1import logging 2import itertools 3 4from curtsies import fsarray, fmtstr, FSArray 5from curtsies.formatstring import linesplit 6from curtsies.fmtfuncs import bold 7 8from .parse import func_for_letter 9 10logger = logging.getLogger(__name__) 11 12# All paint functions should 13# * return an array of the width they were asked for 14# * return an array not taller than the height they were asked for 15 16 17def display_linize(msg, columns, blank_line=False): 18 """Returns lines obtained by splitting msg over multiple lines. 19 20 Warning: if msg is empty, returns an empty list of lines""" 21 if not msg: 22 return [""] if blank_line else [] 23 msg = fmtstr(msg) 24 try: 25 display_lines = list(msg.width_aware_splitlines(columns)) 26 # use old method if wcwidth can't determine width of msg 27 except ValueError: 28 display_lines = [ 29 msg[start:end] 30 for start, end in zip( 31 range(0, len(msg), columns), 32 range(columns, len(msg) + columns, columns), 33 ) 34 ] 35 return display_lines 36 37 38def paint_history(rows, columns, display_lines): 39 lines = [] 40 for r, line in zip(range(rows), display_lines[-rows:]): 41 lines.append(fmtstr(line[:columns])) 42 r = fsarray(lines, width=columns) 43 assert r.shape[0] <= rows, repr(r.shape) + " " + repr(rows) 44 assert r.shape[1] <= columns, repr(r.shape) + " " + repr(columns) 45 return r 46 47 48def paint_current_line(rows, columns, current_display_line): 49 lines = display_linize(current_display_line, columns, True) 50 return fsarray(lines, width=columns) 51 52 53def paginate(rows, matches, current, words_wide): 54 if current not in matches: 55 current = matches[0] 56 per_page = rows * words_wide 57 current_page = matches.index(current) // per_page 58 return matches[per_page * current_page : per_page * (current_page + 1)] 59 60 61def matches_lines(rows, columns, matches, current, config, match_format): 62 highlight_color = func_for_letter(config.color_scheme["operator"].lower()) 63 64 if not matches: 65 return [] 66 color = func_for_letter(config.color_scheme["main"]) 67 max_match_width = max(len(m) for m in matches) 68 words_wide = max(1, (columns - 1) // (max_match_width + 1)) 69 matches = [match_format(m) for m in matches] 70 if current: 71 current = match_format(current) 72 73 matches = paginate(rows, matches, current, words_wide) 74 75 result = [ 76 fmtstr(" ").join( 77 color(m.ljust(max_match_width)) 78 if m != current 79 else highlight_color(m.ljust(max_match_width)) 80 for m in matches[i : i + words_wide] 81 ) 82 for i in range(0, len(matches), words_wide) 83 ] 84 85 logger.debug("match: %r" % current) 86 logger.debug("matches_lines: %r" % result) 87 return result 88 89 90def formatted_argspec(funcprops, arg_pos, columns, config): 91 # Pretty directly taken from bpython.cli 92 func = funcprops.func 93 args = funcprops.argspec.args 94 kwargs = funcprops.argspec.defaults 95 _args = funcprops.argspec.varargs 96 _kwargs = funcprops.argspec.varkwargs 97 is_bound_method = funcprops.is_bound_method 98 kwonly = funcprops.argspec.kwonly 99 kwonly_defaults = funcprops.argspec.kwonly_defaults or dict() 100 101 arg_color = func_for_letter(config.color_scheme["name"]) 102 func_color = func_for_letter(config.color_scheme["name"].swapcase()) 103 punctuation_color = func_for_letter(config.color_scheme["punctuation"]) 104 token_color = func_for_letter(config.color_scheme["token"]) 105 bolds = { 106 token_color: lambda x: bold(token_color(x)), 107 arg_color: lambda x: bold(arg_color(x)), 108 } 109 110 s = func_color(func) + arg_color(": (") 111 112 if is_bound_method and isinstance(arg_pos, int): 113 # TODO what values could this have? 114 arg_pos += 1 115 116 for i, arg in enumerate(args): 117 kw = None 118 if kwargs and i >= len(args) - len(kwargs): 119 kw = str(kwargs[i - (len(args) - len(kwargs))]) 120 color = token_color if arg_pos in (i, arg) else arg_color 121 if i == arg_pos or arg == arg_pos: 122 color = bolds[color] 123 124 s += color(arg) 125 126 if kw is not None: 127 s += punctuation_color("=") 128 s += token_color(kw) 129 130 if i != len(args) - 1: 131 s += punctuation_color(", ") 132 133 if _args: 134 if args: 135 s += punctuation_color(", ") 136 s += token_color(f"*{_args}") 137 138 if kwonly: 139 if not _args: 140 if args: 141 s += punctuation_color(", ") 142 s += punctuation_color("*") 143 marker = object() 144 for arg in kwonly: 145 s += punctuation_color(", ") 146 color = token_color 147 if arg_pos: 148 color = bolds[color] 149 s += color(arg) 150 default = kwonly_defaults.get(arg, marker) 151 if default is not marker: 152 s += punctuation_color("=") 153 s += token_color(repr(default)) 154 155 if _kwargs: 156 if args or _args or kwonly: 157 s += punctuation_color(", ") 158 s += token_color(f"**{_kwargs}") 159 s += punctuation_color(")") 160 161 return linesplit(s, columns) 162 163 164def formatted_docstring(docstring, columns, config): 165 if isinstance(docstring, bytes): 166 docstring = docstring.decode("utf8") 167 elif isinstance(docstring, str): 168 pass 169 else: 170 # TODO: fail properly here and catch possible exceptions in callers. 171 return [] 172 color = func_for_letter(config.color_scheme["comment"]) 173 return sum( 174 ( 175 [ 176 color(x) 177 for x in (display_linize(line, columns) if line else fmtstr("")) 178 ] 179 for line in docstring.split("\n") 180 ), 181 [], 182 ) 183 184 185def paint_infobox( 186 rows, 187 columns, 188 matches, 189 funcprops, 190 arg_pos, 191 match, 192 docstring, 193 config, 194 match_format, 195): 196 """Returns painted completions, funcprops, match, docstring etc.""" 197 if not (rows and columns): 198 return FSArray(0, 0) 199 width = columns - 4 200 from_argspec = ( 201 formatted_argspec(funcprops, arg_pos, width, config) 202 if funcprops 203 else [] 204 ) 205 from_doc = ( 206 formatted_docstring(docstring, width, config) if docstring else [] 207 ) 208 from_matches = ( 209 matches_lines( 210 max(1, rows - len(from_argspec) - 2), 211 width, 212 matches, 213 match, 214 config, 215 match_format, 216 ) 217 if matches 218 else [] 219 ) 220 221 lines = from_argspec + from_matches + from_doc 222 223 def add_border(line): 224 """Add colored borders left and right to a line.""" 225 new_line = border_color(config.left_border + " ") 226 new_line += line.ljust(width)[:width] 227 new_line += border_color(" " + config.right_border) 228 return new_line 229 230 border_color = func_for_letter(config.color_scheme["main"]) 231 232 top_line = border_color( 233 config.left_top_corner 234 + config.top_border * (width + 2) 235 + config.right_top_corner 236 ) 237 bottom_line = border_color( 238 config.left_bottom_corner 239 + config.bottom_border * (width + 2) 240 + config.right_bottom_corner 241 ) 242 243 output_lines = list( 244 itertools.chain((top_line,), map(add_border, lines), (bottom_line,)) 245 ) 246 r = fsarray( 247 output_lines[: min(rows - 1, len(output_lines) - 1)] + output_lines[-1:] 248 ) 249 return r 250 251 252def paint_last_events(rows, columns, names, config): 253 if not names: 254 return fsarray([]) 255 width = min(max(len(name) for name in names), columns - 2) 256 output_lines = [] 257 output_lines.append( 258 config.left_top_corner 259 + config.top_border * width 260 + config.right_top_corner 261 ) 262 for name in reversed(names[max(0, len(names) - (rows - 2)) :]): 263 output_lines.append( 264 config.left_border 265 + name[:width].center(width) 266 + config.right_border 267 ) 268 output_lines.append( 269 config.left_bottom_corner 270 + config.bottom_border * width 271 + config.right_bottom_corner 272 ) 273 return fsarray(output_lines) 274 275 276def paint_statusbar(rows, columns, msg, config): 277 func = func_for_letter(config.color_scheme["main"]) 278 return fsarray([func(msg.ljust(columns))[:columns]]) 279