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