text_win.py 23.4 KB
Newer Older
1 2 3 4 5 6 7 8
"""
TextWin, the window showing the text messages and info messages in poezio.
Can be locked, scrolled, has a separator, etc…
"""

import logging
import curses
from math import ceil, log10
9
from typing import Optional, List, Union
10

Link Mauve's avatar
Link Mauve committed
11
from poezio.windows.base_wins import Win, FORMAT_CHAR
mathieui's avatar
mathieui committed
12
from poezio.windows.funcs import truncate_nick, parse_attrs
13

14 15 16
from poezio import poopt
from poezio.config import config
from poezio.theming import to_curses_attr, get_theme, dump_tuple
17 18 19
from poezio.text_buffer import Message

log = logging.getLogger(__name__)
20 21


Link Mauve's avatar
Link Mauve committed
22 23 24 25
# msg is a reference to the corresponding Message object. text_start and
# text_end are the position delimiting the text in this line.
class Line:
    __slots__ = ('msg', 'start_pos', 'end_pos', 'prepend')
mathieui's avatar
mathieui committed
26

27
    def __init__(self, msg: Message, start_pos: int, end_pos: int, prepend: str) -> None:
Link Mauve's avatar
Link Mauve committed
28 29 30 31 32 33
        self.msg = msg
        self.start_pos = start_pos
        self.end_pos = end_pos
        self.prepend = prepend


34
class BaseTextWin(Win):
35 36 37
    __slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer',
                 'separator_after')

38
    def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
39 40
        if lines_nb_limit is None:
            lines_nb_limit = config.get('max_lines_in_memory')
41
        Win.__init__(self)
42
        self.lines_nb_limit = lines_nb_limit  # type: int
43
        self.pos = 0
44
        # Each new message is built and kept here.
45
        # on resize, we rebuild all the messages
46
        self.built_lines = []  # type: List[Union[None, Line]]
47 48

        self.lock = False
49 50
        self.lock_buffer = []  # type: List[Union[None, Line]]
        self.separator_after = None  # type: Optional[Line]
51

52
    def toggle_lock(self) -> bool:
53 54 55 56 57 58
        if self.lock:
            self.release_lock()
        else:
            self.acquire_lock()
        return self.lock

59
    def acquire_lock(self) -> None:
60 61
        self.lock = True

62
    def release_lock(self) -> None:
63 64 65 66
        for line in self.lock_buffer:
            self.built_lines.append(line)
        self.lock = False

67
    def scroll_up(self, dist: int = 14) -> bool:
68 69 70 71 72 73 74 75
        pos = self.pos
        self.pos += dist
        if self.pos + self.height > len(self.built_lines):
            self.pos = len(self.built_lines) - self.height
            if self.pos < 0:
                self.pos = 0
        return self.pos != pos

76
    def scroll_down(self, dist: int = 14) -> bool:
77 78 79 80 81 82
        pos = self.pos
        self.pos -= dist
        if self.pos <= 0:
            self.pos = 0
        return self.pos != pos

83
    # TODO: figure out the type of history.
mathieui's avatar
mathieui committed
84
    def build_new_message(self,
85
                          message: Message,
mathieui's avatar
mathieui committed
86
                          history=None,
87 88 89 90
                          clean: bool = True,
                          highlight: bool = False,
                          timestamp: bool = False,
                          nick_size: int = 10) -> int:
91 92 93 94 95
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
96
        #pylint: disable=assignment-from-no-return
mathieui's avatar
mathieui committed
97 98
        lines = self.build_message(
            message, timestamp=timestamp, nick_size=nick_size)
99 100 101 102 103 104 105 106 107 108 109
        if self.lock:
            self.lock_buffer.extend(lines)
        else:
            self.built_lines.extend(lines)
        if not lines or not lines[0]:
            return 0
        if clean:
            while len(self.built_lines) > self.lines_nb_limit:
                self.built_lines.pop(0)
        return len(lines)

110
    def build_message(self, message: Message, timestamp: bool = False, nick_size: int = 10) -> List[Union[None, Line]]:
111 112 113 114
        """
        Build a list of lines from a message, without adding it
        to a list
        """
115
        return []
116

117
    def refresh(self) -> None:
118 119
        pass

120
    def write_text(self, y: int, x: int, txt: str) -> None:
121 122 123 124 125
        """
        write the text of a line.
        """
        self.addstr_colored(txt, y, x)

126
    def write_time(self, time: str) -> int:
127 128 129 130
        """
        Write the date on the yth line of the window
        """
        if time:
131 132 133
            color = get_theme().COLOR_TIME_STRING
            curses_color = to_curses_attr(color)
            self._win.attron(curses_color)
134
            self.addstr(time)
135
            self._win.attroff(curses_color)
136
            self.addstr(' ')
mathieui's avatar
mathieui committed
137 138
            return poopt.wcswidth(time) + 1
        return 0
139

140 141
    # TODO: figure out the type of room.
    def resize(self, height: int, width: int, y: int, x: int, room=None) -> None:
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
        if hasattr(self, 'width'):
            old_width = self.width
        else:
            old_width = None
        self._resize(height, width, y, x)
        if room and self.width != old_width:
            self.rebuild_everything(room)

        # reposition the scrolling after resize
        # (see #2450)
        buf_size = len(self.built_lines)
        if buf_size - self.pos < self.height:
            self.pos = buf_size - self.height
            if self.pos < 0:
                self.pos = 0

158 159
    # TODO: figure out the type of room.
    def rebuild_everything(self, room) -> None:
160 161
        self.built_lines = []
        with_timestamps = config.get('show_timestamps')
mathieui's avatar
mathieui committed
162
        nick_size = config.get('max_nick_length')
163
        for message in room.messages:
mathieui's avatar
mathieui committed
164 165 166 167 168
            self.build_new_message(
                message,
                clean=False,
                timestamp=with_timestamps,
                nick_size=nick_size)
169 170 171 172 173
            if self.separator_after is message:
                self.build_new_message(None)
        while len(self.built_lines) > self.lines_nb_limit:
            self.built_lines.pop(0)

174
    def __del__(self) -> None:
mathieui's avatar
mathieui committed
175 176
        log.debug('** TextWin: deleting %s built lines',
                  (len(self.built_lines)))
177 178
        del self.built_lines

mathieui's avatar
mathieui committed
179

180
class TextWin(BaseTextWin):
181 182
    __slots__ = ('highlights', 'hl_pos', 'nb_of_highlights_after_separator')

183
    def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
184 185 186
        BaseTextWin.__init__(self, lines_nb_limit)

        # the Lines of the highlights in that buffer
187
        self.highlights = []  # type: List[Line]
188 189 190 191 192 193 194 195 196 197
        # the current HL position in that list NaN means that we’re not on
        # an hl. -1 is a valid position (it's before the first hl of the
        # list. i.e the separator, in the case where there’s no hl before
        # it.)
        self.hl_pos = float('nan')

        # Keep track of the number of hl after the separator.
        # This is useful to make “go to next highlight“ work after a “move to separator”.
        self.nb_of_highlights_after_separator = 0

198
    def next_highlight(self) -> None:
199 200 201
        """
        Go to the next highlight in the buffer.
        (depending on which highlight was selected before)
Link Mauve's avatar
Link Mauve committed
202
        if the buffer is already positioned on the last, of if there are no
203 204 205
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the next highlight…')
mathieui's avatar
mathieui committed
206 207
        if (not self.highlights or self.hl_pos != self.hl_pos
                or self.hl_pos >= len(self.highlights) - 1):
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
            self.hl_pos = float('nan')
            self.pos = 0
            return
        hl_size = len(self.highlights) - 1
        if self.hl_pos < hl_size:
            self.hl_pos += 1
        else:
            self.hl_pos = hl_size
        log.debug("self.hl_pos = %s", self.hl_pos)
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
mathieui's avatar
mathieui committed
223
                self.highlights = self.highlights[self.hl_pos + 1:]
224 225 226 227 228 229 230 231 232 233
                if not self.highlights:
                    self.hl_pos = float('nan')
                    self.pos = 0
                    return
                self.hl_pos = 0
                hl = self.highlights[0]
        self.pos = len(self.built_lines) - pos - self.height
        if self.pos < 0 or self.pos >= len(self.built_lines):
            self.pos = 0

234
    def previous_highlight(self) -> None:
235 236 237
        """
        Go to the previous highlight in the buffer.
        (depending on which highlight was selected before)
Link Mauve's avatar
Link Mauve committed
238
        if the buffer is already positioned on the first, or if there are no
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the previous highlight…')
        if not self.highlights or self.hl_pos <= 0:
            self.hl_pos = float('nan')
            self.pos = 0
            return
        if self.hl_pos != self.hl_pos:
            self.hl_pos = len(self.highlights) - 1
        else:
            self.hl_pos -= 1
        log.debug("self.hl_pos = %s", self.hl_pos)
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
mathieui's avatar
mathieui committed
257
                self.highlights = self.highlights[self.hl_pos + 1:]
258 259 260 261 262 263 264 265 266 267
                if not self.highlights:
                    self.hl_pos = float('nan')
                    self.pos = 0
                    return
                self.hl_pos = 0
                hl = self.highlights[0]
        self.pos = len(self.built_lines) - pos - self.height
        if self.pos < 0 or self.pos >= len(self.built_lines):
            self.pos = 0

268
    def scroll_to_separator(self) -> None:
269 270 271 272 273
        """
        Scroll until separator is centered. If no separator is
        present, scroll at the top of the window
        """
        if None in self.built_lines:
mathieui's avatar
mathieui committed
274 275
            self.pos = len(self.built_lines) - self.built_lines.index(
                None) - self.height + 1
276 277 278 279 280 281 282 283 284
            if self.pos < 0:
                self.pos = 0
        else:
            self.pos = len(self.built_lines) - self.height + 1
        # Chose a proper position (not too high)
        self.scroll_up(0)
        # Make “next highlight” work afterwards. This makes it easy to
        # review all the highlights since the separator was placed, in
        # the correct order.
mathieui's avatar
mathieui committed
285 286
        self.hl_pos = len(
            self.highlights) - self.nb_of_highlights_after_separator - 1
287 288
        log.debug("self.hl_pos = %s", self.hl_pos)

289
    def remove_line_separator(self) -> None:
290 291 292 293 294 295 296 297
        """
        Remove the line separator
        """
        log.debug('remove_line_separator')
        if None in self.built_lines:
            self.built_lines.remove(None)
            self.separator_after = None

298 299
    # TODO: figure out the type of room.
    def add_line_separator(self, room=None) -> None:
300 301 302 303 304 305 306 307
        """
        add a line separator at the end of messages list
        room is a textbuffer that is needed to get the previous message
        (in case of resize)
        """
        if None not in self.built_lines:
            self.built_lines.append(None)
            self.nb_of_highlights_after_separator = 0
Link Mauve's avatar
Link Mauve committed
308
            log.debug("Resetting number of highlights after separator")
309 310 311
            if room and room.messages:
                self.separator_after = room.messages[-1]

312
    # TODO: figure out the type of history.
mathieui's avatar
mathieui committed
313
    def build_new_message(self,
314
                          message: Message,
mathieui's avatar
mathieui committed
315
                          history=None,
316 317 318 319
                          clean: bool = True,
                          highlight: bool = False,
                          timestamp: bool = False,
                          nick_size: int = 10) -> int:
320 321 322 323 324
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
mathieui's avatar
mathieui committed
325 326
        lines = self.build_message(
            message, timestamp=timestamp, nick_size=nick_size)
327 328 329 330 331 332 333 334 335 336
        if self.lock:
            self.lock_buffer.extend(lines)
        else:
            self.built_lines.extend(lines)
        if not lines or not lines[0]:
            return 0
        if highlight:
            self.highlights.append(lines[0])
            self.nb_of_highlights_after_separator += 1
            log.debug("Number of highlights after separator is now %s",
mathieui's avatar
mathieui committed
337
                      self.nb_of_highlights_after_separator)
338 339 340 341 342
        if clean:
            while len(self.built_lines) > self.lines_nb_limit:
                self.built_lines.pop(0)
        return len(lines)

343
    def build_message(self, message: Optional[Message], timestamp: bool = False, nick_size: int = 10) -> List[Union[None, Line]]:
344 345 346 347 348 349 350 351 352 353
        """
        Build a list of lines from a message, without adding it
        to a list
        """
        if message is None:  # line separator
            return [None]
        txt = message.txt
        if not txt:
            return []
        if len(message.str_time) > 8:
mathieui's avatar
mathieui committed
354
            default_color = (
355
                FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) + '}')  # type: Optional[str]
356 357
        else:
            default_color = None
358
        ret = []  # type: List[Union[None, Line]]
mathieui's avatar
mathieui committed
359
        nick = truncate_nick(message.nickname, nick_size)
360 361
        offset = 0
        if message.ack:
362 363 364 365
            if message.ack > 0:
                offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
            else:
                offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1
366
        if nick:
mathieui's avatar
mathieui committed
367
            offset += poopt.wcswidth(nick) + 2  # + nick + '> ' length
368 369 370
        if message.revisions > 0:
            offset += ceil(log10(message.revisions + 1))
        if message.me:
mathieui's avatar
mathieui committed
371
            offset += 1  # '* ' before and ' ' after
372 373 374 375 376 377 378
        if timestamp:
            if message.str_time:
                offset += 1 + len(message.str_time)
            if get_theme().CHAR_TIME_LEFT and message.str_time:
                offset += 1
            if get_theme().CHAR_TIME_RIGHT and message.str_time:
                offset += 1
mathieui's avatar
mathieui committed
379
        lines = poopt.cut_text(txt, self.width - offset - 1)
Link Mauve's avatar
Link Mauve committed
380
        prepend = default_color if default_color else ''
381
        attrs = []  # type: List[str]
382
        for line in lines:
mathieui's avatar
mathieui committed
383 384 385 386 387
            saved = Line(
                msg=message,
                start_pos=line[0],
                end_pos=line[1],
                prepend=prepend)
388 389 390 391 392 393 394 395 396 397 398
            attrs = parse_attrs(message.txt[line[0]:line[1]], attrs)
            if attrs:
                prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs)
            else:
                if default_color:
                    prepend = default_color
                else:
                    prepend = ''
            ret.append(saved)
        return ret

399
    def refresh(self) -> None:
400 401 402 403 404 405
        log.debug('Refresh: %s', self.__class__.__name__)
        if self.height <= 0:
            return
        if self.pos == 0:
            lines = self.built_lines[-self.height:]
        else:
mathieui's avatar
mathieui committed
406
            lines = self.built_lines[-self.height - self.pos:-self.pos]
407
        with_timestamps = config.get("show_timestamps")
mathieui's avatar
mathieui committed
408
        nick_size = config.get("max_nick_length")
409 410
        self._win.move(0, 0)
        self._win.erase()
mathieui's avatar
mathieui committed
411
        offset = 0
412 413 414 415
        for y, line in enumerate(lines):
            if line:
                msg = line.msg
                if line.start_pos == 0:
mathieui's avatar
mathieui committed
416 417
                    offset = self.write_pre_msg(msg, with_timestamps,
                                                nick_size)
mathieui's avatar
mathieui committed
418
                elif y == 0:
mathieui's avatar
mathieui committed
419 420 421 422 423
                    offset = self.compute_offset(msg, with_timestamps,
                                                 nick_size)
                self.write_text(
                    y, offset,
                    line.prepend + line.msg.txt[line.start_pos:line.end_pos])
424
            else:
mathieui's avatar
mathieui committed
425
                self.write_line_separator(y)
mathieui's avatar
mathieui committed
426
            if y != self.height - 1:
427 428 429
                self.addstr('\n')
        self._win.attrset(0)
        self._refresh()
430

431
    def compute_offset(self, msg, with_timestamps, nick_size) -> int:
mathieui's avatar
mathieui committed
432 433 434 435
        offset = 0
        if with_timestamps and msg.str_time:
            offset += poopt.wcswidth(msg.str_time) + 1

mathieui's avatar
mathieui committed
436
        if not msg.nickname:  # not a message, nothing to do afterwards
mathieui's avatar
mathieui committed
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
            return offset

        nick = truncate_nick(msg.nickname, nick_size)
        offset += poopt.wcswidth(nick)
        if msg.ack:
            if msg.ack > 0:
                offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
            else:
                offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1
        if msg.me:
            offset += 3
        else:
            offset += 2
        if msg.revisions:
            offset += ceil(log10(msg.revisions + 1))
        offset += self.write_revisions(msg)
        return offset

455
    def write_pre_msg(self, msg, with_timestamps, nick_size) -> int:
mathieui's avatar
mathieui committed
456
        offset = 0
mathieui's avatar
mathieui committed
457 458 459
        if with_timestamps:
            offset += self.write_time(msg.str_time)

mathieui's avatar
mathieui committed
460
        if not msg.nickname:  # not a message, nothing to do afterwards
mathieui's avatar
mathieui committed
461 462
            return offset

mathieui's avatar
mathieui committed
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
        nick = truncate_nick(msg.nickname, nick_size)
        offset += poopt.wcswidth(nick)
        if msg.nick_color:
            color = msg.nick_color
        elif msg.user:
            color = msg.user.color
        else:
            color = None
        if msg.ack:
            if msg.ack > 0:
                offset += self.write_ack()
            else:
                offset += self.write_nack()
        if msg.me:
            self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE))
            self.addstr('* ')
            self.write_nickname(nick, color, msg.highlight)
            offset += self.write_revisions(msg)
            self.addstr(' ')
            offset += 3
        else:
            self.write_nickname(nick, color, msg.highlight)
            offset += self.write_revisions(msg)
            self.addstr('> ')
            offset += 2
        return offset

490
    def write_revisions(self, msg) -> int:
mathieui's avatar
mathieui committed
491
        if msg.revisions:
mathieui's avatar
mathieui committed
492 493
            self._win.attron(
                to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE))
mathieui's avatar
mathieui committed
494 495 496 497 498
            self.addstr('%d' % msg.revisions)
            self._win.attrset(0)
            return ceil(log10(msg.revisions + 1))
        return 0

499
    def write_line_separator(self, y) -> None:
500
        char = get_theme().CHAR_NEW_TEXT_SEPARATOR
mathieui's avatar
mathieui committed
501
        self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
mathieui's avatar
mathieui committed
502
                     to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR))
503

504
    def write_ack(self) -> int:
505 506 507 508 509
        color = get_theme().COLOR_CHAR_ACK
        self._win.attron(to_curses_attr(color))
        self.addstr(get_theme().CHAR_ACK_RECEIVED)
        self._win.attroff(to_curses_attr(color))
        self.addstr(' ')
mathieui's avatar
mathieui committed
510
        return poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
511

512
    def write_nack(self) -> int:
513 514 515 516 517
        color = get_theme().COLOR_CHAR_NACK
        self._win.attron(to_curses_attr(color))
        self.addstr(get_theme().CHAR_NACK)
        self._win.attroff(to_curses_attr(color))
        self.addstr(' ')
mathieui's avatar
mathieui committed
518
        return poopt.wcswidth(get_theme().CHAR_NACK) + 1
519

520
    def write_nickname(self, nickname, color, highlight=False) -> None:
521 522 523 524 525 526 527 528 529 530 531 532 533 534
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
        if not nickname:
            return
        if highlight:
            hl_color = get_theme().COLOR_HIGHLIGHT_NICK
            if hl_color == "reverse":
                self._win.attron(curses.A_REVERSE)
            else:
                color = hl_color
        if color:
            self._win.attron(to_curses_attr(color))
mathieui's avatar
mathieui committed
535
        self.addstr(nickname)
536 537 538 539 540
        if color:
            self._win.attroff(to_curses_attr(color))
        if highlight and hl_color == "reverse":
            self._win.attroff(curses.A_REVERSE)

541
    def modify_message(self, old_id, message) -> None:
542 543 544 545
        """
        Find a message, and replace it with a new one
        (instead of rebuilding everything in order to correct a message)
        """
546
        with_timestamps = config.get('show_timestamps')
mathieui's avatar
mathieui committed
547
        nick_size = config.get('max_nick_length')
mathieui's avatar
mathieui committed
548
        for i in range(len(self.built_lines) - 1, -1, -1):
549 550 551 552 553 554
            if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id:
                index = i
                while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id:
                    self.built_lines.pop(index)
                    index -= 1
                index += 1
mathieui's avatar
mathieui committed
555 556
                lines = self.build_message(
                    message, timestamp=with_timestamps, nick_size=nick_size)
557 558 559 560 561
                for line in lines:
                    self.built_lines.insert(index, line)
                    index += 1
                break

562
    def __del__(self) -> None:
mathieui's avatar
mathieui committed
563 564
        log.debug('** TextWin: deleting %s built lines',
                  (len(self.built_lines)))
565 566
        del self.built_lines

mathieui's avatar
mathieui committed
567

568
class XMLTextWin(BaseTextWin):
569 570
    __slots__ = ()

571
    def __init__(self) -> None:
572 573
        BaseTextWin.__init__(self)

574
    def refresh(self) -> None:
575 576 577 578 579 580 581
        log.debug('Refresh: %s', self.__class__.__name__)
        theme = get_theme()
        if self.height <= 0:
            return
        if self.pos == 0:
            lines = self.built_lines[-self.height:]
        else:
mathieui's avatar
mathieui committed
582
            lines = self.built_lines[-self.height - self.pos:-self.pos]
583 584 585 586 587 588 589 590 591 592 593 594 595
        self._win.move(0, 0)
        self._win.erase()
        for y, line in enumerate(lines):
            if line:
                msg = line.msg
                if line.start_pos == 0:
                    if msg.nickname == theme.CHAR_XML_OUT:
                        color = theme.COLOR_XML_OUT
                    elif msg.nickname == theme.CHAR_XML_IN:
                        color = theme.COLOR_XML_IN
                    self.write_time(msg.str_time)
                    self.write_prefix(msg.nickname, color)
                    self.addstr(' ')
mathieui's avatar
mathieui committed
596
            if y != self.height - 1:
597 598 599 600 601 602 603 604 605 606 607 608 609 610
                self.addstr('\n')
        self._win.attrset(0)
        for y, line in enumerate(lines):
            offset = 0
            # Offset for the timestamp (if any) plus a space after it
            offset += len(line.msg.str_time)
            # space
            offset += 1

            # Offset for the prefix
            offset += poopt.wcswidth(truncate_nick(line.msg.nickname))
            # space
            offset += 1

mathieui's avatar
mathieui committed
611 612 613 614
            self.write_text(
                y, offset,
                line.prepend + line.msg.txt[line.start_pos:line.end_pos])
            if y != self.height - 1:
615 616 617 618
                self.addstr('\n')
        self._win.attrset(0)
        self._refresh()

619
    def build_message(self, message: Message, timestamp: bool = False, nick_size: int = 10) -> List[Line]:
620 621 622
        txt = message.txt
        ret = []
        default_color = None
mathieui's avatar
mathieui committed
623
        nick = truncate_nick(message.nickname, nick_size)
624 625
        offset = 0
        if nick:
mathieui's avatar
mathieui committed
626
            offset += poopt.wcswidth(nick) + 1  # + nick + ' ' length
627 628 629 630 631 632
        if message.str_time:
            offset += 1 + len(message.str_time)
        if get_theme().CHAR_TIME_LEFT and message.str_time:
            offset += 1
        if get_theme().CHAR_TIME_RIGHT and message.str_time:
            offset += 1
mathieui's avatar
mathieui committed
633
        lines = poopt.cut_text(txt, self.width - offset - 1)
Link Mauve's avatar
Link Mauve committed
634
        prepend = default_color if default_color else ''
635
        attrs = []  # type: List[str]
636
        for line in lines:
mathieui's avatar
mathieui committed
637 638 639 640 641
            saved = Line(
                msg=message,
                start_pos=line[0],
                end_pos=line[1],
                prepend=prepend)
642 643 644 645 646 647 648 649 650 651 652
            attrs = parse_attrs(message.txt[line[0]:line[1]], attrs)
            if attrs:
                prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs)
            else:
                if default_color:
                    prepend = default_color
                else:
                    prepend = ''
            ret.append(saved)
        return ret

653
    def write_prefix(self, nickname, color) -> None:
654 655 656
        self._win.attron(to_curses_attr(color))
        self.addstr(truncate_nick(nickname))
        self._win.attroff(to_curses_attr(color))