windows.py 87.2 KB
Newer Older
louiz’'s avatar
louiz’ committed
1
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
2 3 4 5
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7

8
"""
9 10 11 12
Define all the windows.
A window is a little part of the screen, for example the input window,
the text window, the roster window, etc.
A Tab (see tab.py) is composed of multiple Windows
13 14
"""

15

16 17 18
import logging
log = logging.getLogger(__name__)

19
import curses
louiz’'s avatar
louiz’ committed
20
import string
mathieui's avatar
mathieui committed
21
from datetime import datetime
22
from math import ceil, log10
23
from config import config
24

25
from threading import RLock
26

27
from contact import Contact, Resource
28
from roster import RosterGroup
29
import poopt
30

31
from common import safeJID
mathieui's avatar
mathieui committed
32
import common
33

louiz’'s avatar
louiz’ committed
34 35
import core
import singleton
36 37
import collections

mathieui's avatar
mathieui committed
38
from theming import get_theme, to_curses_attr, read_tuple
39

louiz’'s avatar
louiz’ committed
40 41 42
FORMAT_CHAR = '\x19'
# These are non-printable chars, so they should never appear in the input, I
# guess. But maybe we can find better chars that are even less reasky.
43
format_chars = ['\x0E', '\x0F', '\x10', '\x11', '\x12','\x13', '\x14', '\x15','\x16', '\x17', '\x18']
louiz’'s avatar
louiz’ committed
44

45
allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7')
louiz’'s avatar
louiz’ committed
46 47 48
# msg is a reference to the corresponding Message tuple. text_start and text_end are the position
# delimiting the text in this line.
# first is a bool telling if this is the first line of the message.
49
Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend')
50

51
g_lock = RLock()
52

53
LINES_NB_LIMIT = 4096
54

louiz’'s avatar
louiz’ committed
55 56 57 58 59 60 61 62 63 64
def find_first_format_char(text):
    pos = -1
    for char in format_chars:
        p = text.find(char)
        if p == -1:
            continue
        if pos == -1 or p < pos:
            pos = p
    return pos

65 66 67 68
def truncate_nick(nick, size=None):
    size = size or config.get('max_nick_length', 25)
    if size < 1:
        size = 1
69
    if nick and len(nick) > size:
louiz’'s avatar
louiz’ committed
70 71 72
        return nick[:size]+'…'
    return nick

73
def parse_attrs(text, previous=None):
louiz’'s avatar
louiz’ committed
74
    next_attr_char = text.find(FORMAT_CHAR)
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    if previous:
        attrs = previous
    else:
        attrs = []
    while next_attr_char != -1 and text:
        if next_attr_char + 1 < len(text):
            attr_char = text[next_attr_char+1].lower()
        else:
            attr_char = str()
        if attr_char == 'o':
            attrs = []
        elif attr_char == 'u':
            attrs.append('u')
        elif attr_char == 'b':
            attrs.append('b')
        if attr_char in string.digits and attr_char != '':
            color_str = text[next_attr_char+1:text.find('}', next_attr_char)]
            if color_str:
                attrs.append(color_str + '}')
            text = text[next_attr_char+len(color_str)+2:]
        else:
            text = text[next_attr_char+2:]
louiz’'s avatar
louiz’ committed
97
        next_attr_char = text.find(FORMAT_CHAR)
98 99
    return attrs

louiz’'s avatar
louiz’ committed
100

101
class Win(object):
louiz’'s avatar
louiz’ committed
102
    _win_core = None
louiz’'s avatar
louiz’ committed
103
    _tab_win = None
104
    def __init__(self):
105
        self._win = None
106

louiz’'s avatar
louiz’ committed
107
    def _resize(self, height, width, y, x):
108
        if height == 0 or width == 0:
109
            self.height, self.width = height, width
110
            return
111
        self.height, self.width, self.x, self.y = height, width, x, y
louiz’'s avatar
louiz’ committed
112 113 114 115
        try:
            self._win = Win._tab_win.derwin(height, width, y, x)
        except:
            log.debug('DEBUG: mvwin returned ERR. Please investigate')
louiz’'s avatar
louiz’ committed
116 117

        # If this ever fail, uncomment that ^
118

louiz’'s avatar
louiz’ committed
119 120 121 122
    def resize(self, height, width, y, x):
        """
        Override if something has to be done on resize
        """
123 124
        with g_lock:
            self._resize(height, width, y, x)
louiz’'s avatar
louiz’ committed
125

126 127
    def _refresh(self):
        self._win.noutrefresh()
128

129 130
    def addnstr(self, *args):
        """
131
        Safe call to addnstr
132 133
        """
        try:
134
            self._win.addnstr(*args)
135 136 137 138 139
        except:
            pass

    def addstr(self, *args):
        """
140
        Safe call to addstr
141
        """
142
        try:
143
            self._win.addstr(*args)
144 145
        except:
            pass
146

147 148 149 150 151 152
    def move(self, y, x):
        try:
            self._win.move(y, x)
        except:
            self._win.move(0, 0)

153
    def addstr_colored(self, text, y=None, x=None):
154 155 156 157 158
        """
        Write a string on the window, setting the
        attributes as they are in the string.
        For example:
        \x19bhello → hello in bold
159
        \x191}Bonj\x192}our → 'Bonj' in red and 'our' in green
160 161 162 163 164
        next_attr_char is the \x19 delimiter
        attr_char is the char following it, it can be
        one of 'u', 'b', 'c[0-9]'
        """
        if y is not None and x is not None:
165
            self.move(y, x)
louiz’'s avatar
louiz’ committed
166
        next_attr_char = text.find(FORMAT_CHAR)
167 168 169 170 171 172 173 174 175 176 177 178 179
        while next_attr_char != -1 and text:
            if next_attr_char + 1 < len(text):
                attr_char = text[next_attr_char+1].lower()
            else:
                attr_char = str()
            if next_attr_char != 0:
                self.addstr(text[:next_attr_char])
            if attr_char == 'o':
                self._win.attrset(0)
            elif attr_char == 'u':
                self._win.attron(curses.A_UNDERLINE)
            elif attr_char == 'b':
                self._win.attron(curses.A_BOLD)
180
            if (attr_char in string.digits or attr_char == '-') and attr_char != '':
181
                color_str = text[next_attr_char+1:text.find('}', next_attr_char)]
182 183 184 185 186 187 188 189 190 191 192
                if ',' in color_str:
                    tup, char = read_tuple(color_str)
                    self._win.attron(to_curses_attr(tup))
                    if char:
                        if char == 'o':
                            self._win.attrset(0)
                        elif char == 'u':
                            self._win.attron(curses.A_UNDERLINE)
                        elif char == 'b':
                            self._win.attron(curses.A_BOLD)
                elif color_str:
louiz’'s avatar
louiz’ committed
193
                    self._win.attron(to_curses_attr((int(color_str), -1)))
louiz’'s avatar
louiz’ committed
194
                text = text[next_attr_char+len(color_str)+2:]
195 196
            else:
                text = text[next_attr_char+2:]
louiz’'s avatar
louiz’ committed
197
            next_attr_char = text.find(FORMAT_CHAR)
198
        self.addstr(text)
199

louiz’'s avatar
louiz’ committed
200
    def finish_line(self, color=None):
201 202 203
        """
        Write colored spaces until the end of line
        """
204
        (y, x) = self._win.getyx()
205
        size = self.width-x
louiz’'s avatar
louiz’ committed
206
        if color:
207
            self.addnstr(' '*size, size, to_curses_attr(color))
louiz’'s avatar
louiz’ committed
208 209
        else:
            self.addnstr(' '*size, size)
210

louiz’'s avatar
louiz’ committed
211 212 213 214 215 216
    @property
    def core(self):
        if not Win._win_core:
            Win._win_core = singleton.Singleton(core.Core)
        return Win._win_core

217
class UserList(Win):
218 219
    def __init__(self):
        Win.__init__(self)
220
        self.pos = 0
221

222
    def scroll_up(self):
223
        self.pos += self.height-1
mathieui's avatar
mathieui committed
224
        return True
225 226

    def scroll_down(self):
mathieui's avatar
mathieui committed
227
        pos = self.pos
228
        self.pos -= self.height-1
229 230
        if self.pos < 0:
            self.pos = 0
mathieui's avatar
mathieui committed
231
        return self.pos != pos
232 233

    def draw_plus(self, y):
234
        self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR))
235

236
    def refresh(self, users):
237
        log.debug('Refresh: %s',self.__class__.__name__)
238
        if config.get("hide_user_list", False):
239
            return # do not refresh if this win is hidden.
240
        with g_lock:
241
            self._win.erase()
242 243 244 245 246 247 248 249
            if config.get('user_list_sort', 'desc').lower() == 'asc':
                y, x = self._win.getmaxyx()
                y -= 1
                users = sorted(users, reverse=True)
            else:
                y = 0
                users = sorted(users)

250 251 252 253
            if len(users) < self.height:
                self.pos = 0
            elif self.pos >= len(users) - self.height and self.pos != 0:
                self.pos = len(users) - self.height
254
            for user in users[self.pos:]:
255 256
                self.draw_role_affiliation(y, user)
                self.draw_status_chatstate(y, user)
257
                self.addstr(y, 2, poopt.cut_by_columns(user.nick, self.width-2), to_curses_attr(user.color))
258 259
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    y -= 1
260
                else:
261
                    y += 1
262 263
                if y == self.height:
                    break
264 265
            # draw indicators of position in the list
            if self.pos > 0:
266 267 268 269
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    self.draw_plus(self.height-1)
                else:
                    self.draw_plus(0)
270
            if self.pos + self.height < len(users):
271 272 273 274
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    self.draw_plus(0)
                else:
                    self.draw_plus(self.height-1)
275
            self._refresh()
276

277
    def draw_role_affiliation(self, y, user):
278 279 280
        theme = get_theme()
        color = theme.color_role(user.role)
        symbol = theme.char_affiliation(user.affiliation)
281 282 283
        self.addstr(y, 1, symbol, to_curses_attr(color))

    def draw_status_chatstate(self, y, user):
284
        show_col = get_theme().color_show(user.show)
285 286 287 288 289 290 291 292 293 294
        if user.chatstate == 'composing':
            char = get_theme().CHAR_CHATSTATE_COMPOSING
        elif user.chatstate == 'active':
            char = get_theme().CHAR_CHATSTATE_ACTIVE
        elif user.chatstate == 'paused':
            char = get_theme().CHAR_CHATSTATE_PAUSED
        else:
            char = get_theme().CHAR_STATUS
        self.addstr(y, 0, char, to_curses_attr(show_col))

louiz’'s avatar
louiz’ committed
295
    def resize(self, height, width, y, x):
296 297 298 299 300
        with g_lock:
            self._resize(height, width, y, x)
            self._win.attron(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR))
            self._win.vline(0, 0, curses.ACS_VLINE, self.height)
            self._win.attroff(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR))
301

302
class Topic(Win):
303 304
    def __init__(self):
        Win.__init__(self)
305
        self._message = ''
306

307
    def refresh(self, topic=None):
308
        log.debug('Refresh: %s',self.__class__.__name__)
309
        with g_lock:
310
            self._win.erase()
311 312 313 314
            if topic:
                msg = topic[:self.width-1]
            else:
                msg = self._message[:self.width-1]
315
            self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR))
316
            (y, x) = self._win.getyx()
317 318 319
            remaining_size = self.width - x
            if remaining_size:
                self.addnstr(' '*remaining_size, remaining_size,
320
                             to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
321
            self._refresh()
322

323 324 325
    def set_message(self, message):
        self._message = message

326
class GlobalInfoBar(Win):
327 328
    def __init__(self):
        Win.__init__(self)
329

louiz’'s avatar
louiz’ committed
330
    def refresh(self):
331
        log.debug('Refresh: %s',self.__class__.__name__)
332
        with g_lock:
333
            self._win.erase()
334
            self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
mathieui's avatar
mathieui committed
335

336 337 338 339
            create_gaps = config.get('create_gaps', False)
            show_names = config.get('show_tab_names', False)
            show_nums = config.get('show_tab_numbers', True)
            use_nicks = config.get('use_tab_nicks', True)
mathieui's avatar
mathieui committed
340 341 342 343
            # ignore any remaining gap tabs if the feature is not enabled
            sorted_tabs = [tab for tab in self.core.tabs if tab] if not create_gaps else self.core.tabs[:]
            for nb, tab in enumerate(sorted_tabs):
                if not tab: continue
344
                color = tab.color
345
                if not config.get('show_inactive_tabs', True) and\
346
                        color is get_theme().COLOR_TAB_NORMAL:
347
                    continue
348
                try:
349
                    if show_nums or not show_names:
mathieui's avatar
mathieui committed
350
                        self.addstr("%s" % str(nb), to_curses_attr(color))
351 352 353 354 355 356 357
                        if show_names:
                            self.addstr(' ', to_curses_attr(color))
                    if show_names:
                        if use_nicks:
                            self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color))
                        else:
                            self.addstr("%s" % str(tab.get_name()), to_curses_attr(color))
358
                    self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
359 360
                except:             # end of line
                    break
361
            (y, x) = self._win.getyx()
362
            self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
363
            (y, x) = self._win.getyx()
364 365
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
366
                         to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
367
            self._refresh()
368

louiz’'s avatar
louiz’ committed
369 370 371 372 373 374 375 376 377
class VerticalGlobalInfoBar(Win):
    def __init__(self, scr):
        Win.__init__(self)
        self._win = scr

    def refresh(self):
        with g_lock:
            height, width = self._win.getmaxyx()
            self._win.erase()
mathieui's avatar
mathieui committed
378
            sorted_tabs = [tab for tab in self.core.tabs if tab]
379
            if not config.get('show_inactive_tabs', True):
louiz’'s avatar
louiz’ committed
380
                sorted_tabs = [tab for tab in sorted_tabs if\
mathieui's avatar
mathieui committed
381
                                   tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL]
louiz’'s avatar
louiz’ committed
382
            nb_tabs = len(sorted_tabs)
383
            use_nicks = config.get('use_tab_nicks', True)
louiz’'s avatar
louiz’ committed
384 385 386 387 388 389 390 391 392 393 394 395 396 397
            if nb_tabs >= height:
                for y, tab in enumerate(sorted_tabs):
                    if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT:
                        pos = y
                        break
                # center the current tab as much as possible
                if pos < height//2:
                    sorted_tabs = sorted_tabs[:height]
                elif nb_tabs - pos <= height//2:
                    sorted_tabs = sorted_tabs[-height:]
                else:
                    sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2]
            for y, tab in enumerate(sorted_tabs):
                color = tab.vertical_color
398
                self.addstr(y if config.get('vertical_tab_list_sort', 'desc') != 'asc' else height - y - 1, 0, "%2d" % tab.nb, to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER))
louiz’'s avatar
louiz’ committed
399
                self.addstr('.')
400 401 402 403
                if use_nicks:
                    self.addnstr("%s" % tab.get_nick(), width - 4, to_curses_attr(color))
                else:
                    self.addnstr("%s" % tab.get_name(), width - 4, to_curses_attr(color))
404 405 406
            self._win.attron(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR))
            self._win.vline(0, width-1, curses.ACS_VLINE, height)
            self._win.attroff(to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR))
louiz’'s avatar
louiz’ committed
407 408
            self._refresh()

409 410 411 412 413
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
414 415
    def __init__(self):
        Win.__init__(self)
416

417
    def print_scroll_position(self, window):
418
        """
419
        Print, like in Weechat, a -MORE(n)- where n
420 421 422
        is the number of available lines to scroll
        down
        """
423
        if window.pos > 0:
louiz’'s avatar
louiz’ committed
424
            plus = ' -MORE(%s)-' % window.pos
425
            self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER))
426

mathieui's avatar
mathieui committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
class XMLInfoWin(InfoWin):
    """
    Info about the latest xml filter used and the state of the buffer.
    """
    def __init__(self):
        InfoWin.__init__(self)

    def refresh(self, filter_t='', filter='', window=None):
        log.debug('Refresh: %s', self.__class__.__name__)
        with g_lock:
            self._win.erase()
            if not filter_t:
                self.addstr('[No filter]', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
            else:
                info = '[%s] %s' % (filter_t, filter)
                self.addstr(info, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
            self.print_scroll_position(window)
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
            self._refresh()

447 448
class PrivateInfoWin(InfoWin):
    """
449
    The line above the information window, displaying informations
450 451
    about the MUC user we are talking to
    """
452 453
    def __init__(self):
        InfoWin.__init__(self)
454

455
    def refresh(self, name, window, chatstate, informations):
456
        log.debug('Refresh: %s',self.__class__.__name__)
457
        with g_lock:
458
            self._win.erase()
louiz’'s avatar
louiz’ committed
459
            self.write_room_name(name)
460
            self.print_scroll_position(window)
461
            self.write_chatstate(chatstate)
462
            self.write_additional_informations(informations, name)
463
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
464
            self._refresh()
465

466 467 468 469 470 471 472 473
    def write_additional_informations(self, informations, jid):
        """
        Write all informations added by plugins by getting the
        value returned by the callbacks.
        """
        for key in informations:
            self.addstr(informations[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))

louiz’'s avatar
louiz’ committed
474
    def write_room_name(self, name):
475
        jid = safeJID(name)
476
        room_name, nick = jid.bare, jid.resource
477
        self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME))
478
        txt = ' from room %s' % room_name
479
        self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
480

481 482
    def write_chatstate(self, state):
        if state:
483
            self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
484

485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
class MucListInfoWin(InfoWin):
    """
    The live above the information window, displaying informations
    about the muc server being listed
    """
    def __init__(self, message=''):
        InfoWin.__init__(self)
        self.message = message

    def refresh(self, name=None):
        log.debug('Refresh: %s',self.__class__.__name__)
        with g_lock:
            self._win.erase()
            if name:
                self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
            else:
                self.addstr(self.message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
            self._refresh()

505 506 507
class ConversationInfoWin(InfoWin):
    """
    The line above the information window, displaying informations
508
    about the user we are talking to
509
    """
510

511 512
    def __init__(self):
        InfoWin.__init__(self)
513

514
    def refresh(self, jid, contact, window, chatstate, informations):
515 516 517
        # contact can be None, if we receive a message
        # from someone not in our roster. In this case, we display
        # only the maximum information from the message we can get.
518
        log.debug('Refresh: %s',self.__class__.__name__)
519
        jid = safeJID(jid)
520 521
        if contact:
            if jid.resource:
522
                resource = contact[jid.full]
523 524 525 526 527
            else:
                resource = contact.get_highest_priority_resource()
        else:
            resource = None
        # if contact is None, then resource is None too: user is not in the roster
528
        # so we know almost nothing about it
529 530 531
        # If contact is a Contact, then
        # resource can now be a Resource: user is in the roster and online
        # or resource is None: user is in the roster but offline
532
        with g_lock:
533
            self._win.erase()
534 535 536
            self.write_contact_jid(jid)
            self.write_contact_informations(contact)
            self.write_resource_information(resource)
537
            self.print_scroll_position(window)
538
            self.write_chatstate(chatstate)
539
            self.write_additional_informations(informations, jid)
540
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
541
            self._refresh()
542

543 544 545 546 547 548 549
    def write_additional_informations(self, informations, jid):
        """
        Write all informations added by plugins by getting the
        value returned by the callbacks.
        """
        for key in informations:
            self.addstr(informations[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
louiz’'s avatar
louiz’ committed
550

551 552 553 554 555 556 557
    def write_resource_information(self, resource):
        """
        Write the informations about the resource
        """
        if not resource:
            presence = "unavailable"
        else:
558
            presence = resource.presence
559
        color = get_theme().color_show(presence)
560
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
561
        self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color))
562
        self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
563 564 565 566 567

    def write_contact_informations(self, contact):
        """
        Write the informations about the contact
        """
568
        if not contact:
569
            self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
570
            return
571 572 573
        display_name = contact.name
        if display_name:
            self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
574 575 576 577 578

    def write_contact_jid(self, jid):
        """
        Just write the jid that we are talking to
        """
579 580 581
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
        self.addstr(jid.full, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME))
        self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
582

583 584
    def write_chatstate(self, state):
        if state:
585
            self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
586

587 588 589 590 591 592 593 594 595 596 597 598
class DynamicConversationInfoWin(ConversationInfoWin):
    def write_contact_jid(self, jid):
        """
        Just displays the resource in an other color
        """
        log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s" % jid.resource)
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
        self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME))
        if jid.resource:
            self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE))
        self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))

599 600 601 602
class ConversationStatusMessageWin(InfoWin):
    """
    The upper bar displaying the status message of the contact
    """
603 604
    def __init__(self):
        InfoWin.__init__(self)
605 606

    def refresh(self, jid, contact):
607
        log.debug('Refresh: %s',self.__class__.__name__)
608
        jid = safeJID(jid)
609 610
        if contact:
            if jid.resource:
611
                resource = contact[jid.full]
612 613
            else:
                resource = contact.get_highest_priority_resource()
614
        else:
615 616 617 618 619
            resource = None
        with g_lock:
            self._win.erase()
            if resource:
                self.write_status_message(resource)
620
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
621 622 623
            self._refresh()

    def write_status_message(self, resource):
624
        self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
625

626 627 628 629 630
class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
631 632
    def __init__(self):
        InfoWin.__init__(self)
633

634
    def refresh(self, room, window=None):
635
        log.debug('Refresh: %s',self.__class__.__name__)
636
        with g_lock:
637
            self._win.erase()
638
            self.write_room_name(room)
639
            self.write_participants_number(room)
640 641 642
            self.write_own_nick(room)
            self.write_disconnected(room)
            self.write_role(room)
643 644
            if window:
                self.print_scroll_position(window)
645
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
646
            self._refresh()
647 648

    def write_room_name(self, room):
649 650
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
        self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
651 652 653 654 655 656
        self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))

    def write_participants_number(self, room):
        self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
        self.addstr(str(len(room.users)), to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
        self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
657 658 659 660 661 662

    def write_disconnected(self, room):
        """
        Shows a message if the room is not joined
        """
        if not room.joined:
663
            self.addstr(' -!- Not connected ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
664

665 666 667 668 669 670 671
    def write_own_nick(self, room):
        """
        Write our own nick in the info bar
        """
        nick = room.own_nick
        if not nick:
            return
672
        self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688

    def write_role(self, room):
        """
        Write our own role and affiliation
        """
        own_user = None
        for user in room.users:
            if user.nick == room.own_nick:
                own_user = user
                break
        if not own_user:
            return
        txt = ' ('
        if own_user.affiliation != 'none':
            txt += own_user.affiliation+', '
        txt += own_user.role+')'
689
        self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
690

691
class TextWin(Win):
692
    def __init__(self, lines_nb_limit=config.get('max_lines_in_memory', 2048)):
693
        Win.__init__(self)
694
        self.lines_nb_limit = lines_nb_limit
695 696 697
        self.pos = 0
        self.built_lines = []   # Each new message is built and kept here.
        # on resize, we rebuild all the messages
698

mathieui's avatar
mathieui committed
699 700
        self.lock = False
        self.lock_buffer = []
701 702

        # the Lines of the highlights in that buffer
703
        self.highlights = []
704 705 706 707 708 709 710 711 712
        # 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
mathieui's avatar
mathieui committed
713

714 715
        self.separator_after = None

mathieui's avatar
mathieui committed
716 717 718 719 720
    def toggle_lock(self):
        if self.lock:
            self.release_lock()
        else:
            self.acquire_lock()
mathieui's avatar
mathieui committed
721
        return self.lock
mathieui's avatar
mathieui committed
722 723 724 725 726 727 728 729

    def acquire_lock(self):
        self.lock = True

    def release_lock(self):
        for line in self.lock_buffer:
            self.built_lines.append(line)
        self.lock = False
730

731 732 733 734 735 736 737 738
    def next_highlight(self):
        """
        Go to the next highlight in the buffer.
        (depending on which highlight was selected before)
        if the buffer is already positionned on the last, of if there are no
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the next highlight…')
739
        if not self.highlights or self.hl_pos != self.hl_pos or \
740
                self.hl_pos == len(self.highlights)-1:
741
            self.hl_pos = float('nan')
742 743 744 745 746 747 748
            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
749
        log.debug("self.hl_pos = %s" % self.hl_pos)
750 751 752 753 754 755 756 757
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
                self.highlights = self.highlights[self.hl_pos+1:]
                if not self.highlights:
758
                    self.hl_pos = float('nan')
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
                    self.pos = 0
                    return
                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

    def previous_highlight(self):
        """
        Go to the previous highlight in the buffer.
        (depending on which highlight was selected before)
        if the buffer is already positionned on the first, or if there are no
        highlights, scroll to the end of the buffer.
        """
        log.debug('Going to the previous highlight…')
774 775
        if not self.highlights or self.hl_pos <= 0:
            self.hl_pos = float('nan')
776 777
            self.pos =  0
            return
778
        if self.hl_pos != self.hl_pos:
779 780 781
            self.hl_pos = len(self.highlights) - 1
        elif self.hl_pos > 0:
            self.hl_pos -= 1
782
        log.debug("self.hl_pos = %s" % self.hl_pos)
783 784 785 786 787 788 789 790
        hl = self.highlights[self.hl_pos]
        pos = None
        while not pos:
            try:
                pos = self.built_lines.index(hl)
            except ValueError:
                self.highlights = self.highlights[self.hl_pos+1:]
                if not self.highlights:
791
                    self.hl_pos = float('nan')
792 793 794 795 796 797 798
                    self.pos = 0
                    return
                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

799
    def scroll_up(self, dist=14):
mathieui's avatar
mathieui committed
800
        pos = self.pos
801
        self.pos += dist
802 803 804 805
        if self.pos + self.height > len(self.built_lines):
            self.pos = len(self.built_lines) - self.height
            if self.pos < 0:
                self.pos = 0
mathieui's avatar
mathieui committed
806
        return self.pos != pos
807 808

    def scroll_down(self, dist=14):
mathieui's avatar
mathieui committed
809
        pos = self.pos
810 811 812
        self.pos -= dist
        if self.pos <= 0:
            self.pos = 0
mathieui's avatar
mathieui committed
813
        return self.pos != pos
814

815 816 817 818 819 820
    def scroll_to_separator(self):
        """
        Scroll until separator is centered. If no separator is
        present, scroll at the top of the window
        """
        if None in self.built_lines:
821 822 823
            self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1
            if self.pos < 0:
                self.pos = 0
824 825 826 827 828 829 830 831 832
        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.
        self.hl_pos = len(self.highlights) - self.nb_of_highlights_after_separator - 1
        log.debug("self.hl_pos = %s" % self.hl_pos)
833

834 835 836 837
    def remove_line_separator(self):
        """
        Remove the line separator
        """
838
        log.debug('remove_line_separator')
839 840
        if None in self.built_lines:
            self.built_lines.remove(None)
841
            self.separator_after = None
842

843
    def add_line_separator(self, room=None):
844 845
        """
        add a line separator at the end of messages list
846 847
        room is a textbuffer that is needed to get the previous message
        (in case of resize)
848 849 850
        """
        if None not in self.built_lines:
            self.built_lines.append(None)
851 852
            self.nb_of_highlights_after_separator = 0
            log.debug("Reseting number of highlights after separator")
853
            if room and room.messages:
854
                self.separator_after = room.messages[-1]
855

856
    def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False):
857 858 859 860 861
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
862 863 864 865 866 867
        lines = self.build_message(message, timestamp=timestamp)
        if self.lock:
            self.lock_buffer.extend(lines)
        else:
            self.built_lines.extend(lines)
        if not lines or not lines[0]:
868
            return 0
869 870
        if highlight:
            self.highlights.append(lines[0])
871 872 873
            self.nb_of_highlights_after_separator += 1
            log.debug("Number of highlights after separator is now %s" % \
                          self.nb_of_highlights_after_separator)
874 875 876 877 878 879 880 881 882 883 884 885
        if clean:
            while len(self.built_lines) > self.lines_nb_limit:
                self.built_lines.pop(0)
        return len(lines)

    def build_message(self, message, timestamp=False):
        """
        Build a list of lines from a message, without adding it
        to a list
        """
        if message is None:  # line separator
            return [None]
886
        txt = message.txt