windows.py 83.5 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
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)

18
19
20
import logging
log = logging.getLogger(__name__)

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

27
from threading import RLock
28

29
from contact import Contact, Resource
30
from roster import RosterGroup
31
import poopt
32

33
from sleekxmpp import JID
34
from common import safeJID
mathieui's avatar
mathieui committed
35
import common
36

louiz’'s avatar
louiz’ committed
37
38
import core
import singleton
39
40
import collections

41
from theming import get_theme, to_curses_attr, read_tuple, dump_tuple
42

43
allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7')
louiz’'s avatar
louiz’ committed
44
45
46
# 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.
47
Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend')
48

49
g_lock = RLock()
50

51
LINES_NB_LIMIT = 4096
52

53
54
55
56
def truncate_nick(nick, size=None):
    size = size or config.get('max_nick_length', 25)
    if size < 1:
        size = 1
57
    if nick and len(nick) > size:
louiz’'s avatar
louiz’ committed
58
59
60
        return nick[:size]+'…'
    return nick

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def parse_attrs(text, previous=None):
    next_attr_char = text.find('\x19')
    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:]
        next_attr_char = text.find('\x19')
    return attrs

louiz’'s avatar
louiz’ committed
88

89
class Win(object):
louiz’'s avatar
louiz’ committed
90
    _win_core = None
louiz’'s avatar
louiz’ committed
91
    _tab_win = None
92
    def __init__(self):
93
        self._win = None
94

louiz’'s avatar
louiz’ committed
95
    def _resize(self, height, width, y, x):
96
        if height == 0 or width == 0:
97
            self.height, self.width = height, width
98
            return
99
        self.height, self.width, self.x, self.y = height, width, x, y
louiz’'s avatar
louiz’ committed
100
101
102
103
        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
104
105

        # If this ever fail, uncomment that ^
106

louiz’'s avatar
louiz’ committed
107
108
109
110
    def resize(self, height, width, y, x):
        """
        Override if something has to be done on resize
        """
111
112
        with g_lock:
            self._resize(height, width, y, x)
louiz’'s avatar
louiz’ committed
113

114
115
    def _refresh(self):
        self._win.noutrefresh()
116

117
118
    def addnstr(self, *args):
        """
119
        Safe call to addnstr
120
121
        """
        try:
122
            self._win.addnstr(*args)
123
124
125
126
127
        except:
            pass

    def addstr(self, *args):
        """
128
        Safe call to addstr
129
        """
130
        try:
131
            self._win.addstr(*args)
132
133
        except:
            pass
134

135
136
137
138
139
140
    def move(self, y, x):
        try:
            self._win.move(y, x)
        except:
            self._win.move(0, 0)

141
    def addstr_colored(self, text, y=None, x=None):
142
143
144
145
146
        """
        Write a string on the window, setting the
        attributes as they are in the string.
        For example:
        \x19bhello → hello in bold
147
        \x191}Bonj\x192}our → 'Bonj' in red and 'our' in green
148
149
150
151
152
        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:
153
            self.move(y, x)
154
        next_attr_char = text.find('\x19')
155
156
157
158
159
160
161
162
163
164
165
166
167
        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)
168
            if (attr_char in string.digits or attr_char == '-') and attr_char != '':
169
                color_str = text[next_attr_char+1:text.find('}', next_attr_char)]
170
171
172
173
174
175
176
177
178
179
180
                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
181
                    self._win.attron(to_curses_attr((int(color_str), -1)))
louiz’'s avatar
louiz’ committed
182
                text = text[next_attr_char+len(color_str)+2:]
183
184
185
186
187
188
189
190
191
192
193
194
195
            else:
                text = text[next_attr_char+2:]
            next_attr_char = text.find('\x19')
        self.addstr(text)

    def addstr_colored_lite(self, text, y=None, x=None):
        """
        Just like addstr_colored, but only handles colors with one digit.
        \x193 is the 3rd color. We do not use any } char in this version
        """
        if y is not None and x is not None:
            self.move(y, x)
        next_attr_char = text.find('\x19')
196
        while next_attr_char != -1:
197
198
199
200
            if next_attr_char + 1 < len(text):
                attr_char = text[next_attr_char+1].lower()
            else:
                attr_char = str()
201
            if next_attr_char != 0:
202
203
                self.addstr(text[:next_attr_char])
            text = text[next_attr_char+2:]
204
205
206
207
208
209
            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)
210
            elif attr_char in string.digits and attr_char != '':
211
                self._win.attron(to_curses_attr((int(attr_char), -1)))
212
213
            next_attr_char = text.find('\x19')
        self.addstr(text)
214

louiz’'s avatar
louiz’ committed
215
    def finish_line(self, color=None):
216
217
218
        """
        Write colored spaces until the end of line
        """
219
        (y, x) = self._win.getyx()
220
        size = self.width-x
louiz’'s avatar
louiz’ committed
221
        if color:
222
            self.addnstr(' '*size, size, to_curses_attr(color))
louiz’'s avatar
louiz’ committed
223
224
        else:
            self.addnstr(' '*size, size)
225

louiz’'s avatar
louiz’ committed
226
227
228
229
230
231
    @property
    def core(self):
        if not Win._win_core:
            Win._win_core = singleton.Singleton(core.Core)
        return Win._win_core

232
class UserList(Win):
233
234
    def __init__(self):
        Win.__init__(self)
235
        self.pos = 0
236

237
    def scroll_up(self):
238
        self.pos += self.height-1
mathieui's avatar
mathieui committed
239
        return True
240
241

    def scroll_down(self):
mathieui's avatar
mathieui committed
242
        pos = self.pos
243
        self.pos -= self.height-1
244
245
        if self.pos < 0:
            self.pos = 0
mathieui's avatar
mathieui committed
246
        return self.pos != pos
247
248

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

251
    def refresh(self, users):
252
        log.debug('Refresh: %s',self.__class__.__name__)
253
254
        if config.get("hide_user_list", "false") == "true":
            return # do not refresh if this win is hidden.
255
        with g_lock:
256
            self._win.erase()
257
258
259
260
261
262
263
264
            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)

265
266
267
268
            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
269
            for user in users[self.pos:]:
270
271
                self.draw_role_affiliation(y, user)
                self.draw_status_chatstate(y, user)
272
                self.addstr(y, 2, poopt.cut_by_columns(user.nick, self.width-2), to_curses_attr(user.color))
273
274
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    y -= 1
275
                else:
276
                    y += 1
277
278
                if y == self.height:
                    break
279
280
            # draw indicators of position in the list
            if self.pos > 0:
281
282
283
284
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    self.draw_plus(self.height-1)
                else:
                    self.draw_plus(0)
285
            if self.pos + self.height < len(users):
286
287
288
289
                if config.get('user_list_sort', 'desc').lower() == 'asc':
                    self.draw_plus(0)
                else:
                    self.draw_plus(self.height-1)
290
            self._refresh()
291

292
    def draw_role_affiliation(self, y, user):
293
294
295
        theme = get_theme()
        color = theme.color_role(user.role)
        symbol = theme.char_affiliation(user.affiliation)
296
297
298
        self.addstr(y, 1, symbol, to_curses_attr(color))

    def draw_status_chatstate(self, y, user):
299
        show_col = get_theme().color_show(user.show)
300
301
302
303
304
305
306
307
308
309
        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
310
    def resize(self, height, width, y, x):
311
312
313
314
315
        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))
316

317
class Topic(Win):
318
319
    def __init__(self):
        Win.__init__(self)
320
        self._message = ''
321

322
    def refresh(self, topic=None):
323
        log.debug('Refresh: %s',self.__class__.__name__)
324
        with g_lock:
325
            self._win.erase()
326
327
328
329
            if topic:
                msg = topic[:self.width-1]
            else:
                msg = self._message[:self.width-1]
330
            self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR))
331
            (y, x) = self._win.getyx()
332
333
334
            remaining_size = self.width - x
            if remaining_size:
                self.addnstr(' '*remaining_size, remaining_size,
335
                             to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
336
            self._refresh()
337

338
339
340
    def set_message(self, message):
        self._message = message

341
class GlobalInfoBar(Win):
342
343
    def __init__(self):
        Win.__init__(self)
344

louiz’'s avatar
louiz’ committed
345
    def refresh(self):
346
        log.debug('Refresh: %s',self.__class__.__name__)
347
        with g_lock:
348
            self._win.erase()
349
            self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
mathieui's avatar
mathieui committed
350
351

            create_gaps = config.getl('create_gaps', 'false') == 'true'
352
353
354
            show_names = config.getl('show_tab_names', 'false') == 'true'
            show_nums = config.getl('show_tab_numbers', 'true') != 'false'
            use_nicks = config.getl('use_tab_nicks', 'true') != 'false'
mathieui's avatar
mathieui committed
355
356
357
358
            # 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
359
                color = tab.color
360
                if config.get('show_inactive_tabs', 'true') == 'false' and\
361
                        color is get_theme().COLOR_TAB_NORMAL:
362
                    continue
363
                try:
364
                    if show_nums or not show_names:
mathieui's avatar
mathieui committed
365
                        self.addstr("%s" % str(nb), to_curses_attr(color))
366
367
368
369
370
371
372
                        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))
373
                    self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
374
375
                except:             # end of line
                    break
376
            (y, x) = self._win.getyx()
377
            self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
378
            (y, x) = self._win.getyx()
379
380
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
381
                         to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
382
            self._refresh()
383

louiz’'s avatar
louiz’ committed
384
385
386
387
388
389
390
391
392
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
393
            sorted_tabs = [tab for tab in self.core.tabs if tab]
louiz’'s avatar
louiz’ committed
394
395
            if config.get('show_inactive_tabs', 'true') == 'false':
                sorted_tabs = [tab for tab in sorted_tabs if\
mathieui's avatar
mathieui committed
396
                                   tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL]
louiz’'s avatar
louiz’ committed
397
            nb_tabs = len(sorted_tabs)
398
            use_nicks = config.getl('use_tab_nicks', 'true') != 'false'
louiz’'s avatar
louiz’ committed
399
400
401
402
403
404
405
406
407
408
409
410
411
412
            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
413
                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
414
                self.addstr('.')
415
416
417
418
                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))
419
420
421
            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
422
423
            self._refresh()

424
425
426
427
428
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
429
430
    def __init__(self):
        Win.__init__(self)
431

432
    def print_scroll_position(self, window):
433
        """
434
        Print, like in Weechat, a -MORE(n)- where n
435
436
437
        is the number of available lines to scroll
        down
        """
438
        if window.pos > 0:
louiz’'s avatar
louiz’ committed
439
            plus = ' -MORE(%s)-' % window.pos
440
            self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER))
441

mathieui's avatar
mathieui committed
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
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()

462
463
class PrivateInfoWin(InfoWin):
    """
464
    The line above the information window, displaying informations
465
466
    about the MUC user we are talking to
    """
467
468
    def __init__(self):
        InfoWin.__init__(self)
469

louiz’'s avatar
louiz’ committed
470
    def refresh(self, name, window, chatstate):
471
        log.debug('Refresh: %s',self.__class__.__name__)
472
        with g_lock:
473
            self._win.erase()
louiz’'s avatar
louiz’ committed
474
            self.write_room_name(name)
475
            self.print_scroll_position(window)
476
            self.write_chatstate(chatstate)
477
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
478
            self._refresh()
479

louiz’'s avatar
louiz’ committed
480
    def write_room_name(self, name):
481
        jid = safeJID(name)
482
        room_name, nick = jid.bare, jid.resource
483
        self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME))
484
        txt = ' from room %s' % room_name
485
        self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
486

487
488
    def write_chatstate(self, state):
        if state:
489
            self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
490

491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
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()

511
512
513
class ConversationInfoWin(InfoWin):
    """
    The line above the information window, displaying informations
514
    about the user we are talking to
515
    """
516

517
518
    def __init__(self):
        InfoWin.__init__(self)
519

520
    def refresh(self, jid, contact, window, chatstate, informations):
521
522
523
        # 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.
524
        log.debug('Refresh: %s',self.__class__.__name__)
525
        jid = safeJID(jid)
526
527
        if contact:
            if jid.resource:
528
                resource = contact[jid.full]
529
530
531
532
533
            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
534
        # so we know almost nothing about it
535
536
537
        # 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
538
        with g_lock:
539
            self._win.erase()
540
541
542
            self.write_contact_jid(jid)
            self.write_contact_informations(contact)
            self.write_resource_information(resource)
543
            self.print_scroll_position(window)
544
            self.write_chatstate(chatstate)
545
            self.write_additional_informations(informations, jid)
546
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
547
            self._refresh()
548

549
550
551
552
553
554
555
    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
556

557
558
559
560
561
562
563
    def write_resource_information(self, resource):
        """
        Write the informations about the resource
        """
        if not resource:
            presence = "unavailable"
        else:
564
            presence = resource.presence
565
        color = get_theme().color_show(presence)
566
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
567
        self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color))
568
        self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
569
570
571
572
573

    def write_contact_informations(self, contact):
        """
        Write the informations about the contact
        """
574
        if not contact:
575
            self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
576
            return
577
578
579
        display_name = contact.name
        if display_name:
            self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
580
581
582
583
584

    def write_contact_jid(self, jid):
        """
        Just write the jid that we are talking to
        """
585
586
587
        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))
588

589
590
    def write_chatstate(self, state):
        if state:
591
            self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
592

593
594
595
596
597
598
599
600
601
602
603
604
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))

605
606
607
608
class ConversationStatusMessageWin(InfoWin):
    """
    The upper bar displaying the status message of the contact
    """
609
610
    def __init__(self):
        InfoWin.__init__(self)
611
612

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

    def write_status_message(self, resource):
630
        self.addstr(resource.status, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
631

632
633
634
635
636
class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
637
638
    def __init__(self):
        InfoWin.__init__(self)
639

640
    def refresh(self, room, window=None):
641
        log.debug('Refresh: %s',self.__class__.__name__)
642
        with g_lock:
643
            self._win.erase()
644
            self.write_room_name(room)
645
            self.write_participants_number(room)
646
647
648
            self.write_own_nick(room)
            self.write_disconnected(room)
            self.write_role(room)
649
650
            if window:
                self.print_scroll_position(window)
651
            self.finish_line(get_theme().COLOR_INFORMATION_BAR)
652
            self._refresh()
653
654

    def write_room_name(self, room):
655
656
        self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
        self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
657
658
659
660
661
662
        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))
663
664
665
666
667
668

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

671
672
673
674
675
676
677
    def write_own_nick(self, room):
        """
        Write our own nick in the info bar
        """
        nick = room.own_nick
        if not nick:
            return
678
        self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694

    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+')'
695
        self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
696

697
class TextWin(Win):
698
    def __init__(self, lines_nb_limit=config.get('max_lines_in_memory', 2048)):
699
        Win.__init__(self)
700
        self.lines_nb_limit = lines_nb_limit
701
702
703
        self.pos = 0
        self.built_lines = []   # Each new message is built and kept here.
        # on resize, we rebuild all the messages
704

mathieui's avatar
mathieui committed
705
706
        self.lock = False
        self.lock_buffer = []
707
708

        # the Lines of the highlights in that buffer
709
        self.highlights = []
710
711
712
713
714
715
716
717
718
        # 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
719

720
721
        self.separator_after = None

mathieui's avatar
mathieui committed
722
723
724
725
726
    def toggle_lock(self):
        if self.lock:
            self.release_lock()
        else:
            self.acquire_lock()
mathieui's avatar
mathieui committed
727
        return self.lock
mathieui's avatar
mathieui committed
728
729
730
731
732
733
734
735

    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
736

737
738
739
740
741
742
743
744
    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…')
745
        if not self.highlights or self.hl_pos != self.hl_pos or \
746
                self.hl_pos == len(self.highlights)-1:
747
            self.hl_pos = float('nan')
748
749
750
751
752
753
754
            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
755
        log.debug("self.hl_pos = %s" % self.hl_pos)
756
757
758
759
760
761
762
763
        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:
764
                    self.hl_pos = float('nan')
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
                    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…')
780
781
        if not self.highlights or self.hl_pos <= 0:
            self.hl_pos = float('nan')
782
783
            self.pos =  0
            return
784
        if self.hl_pos != self.hl_pos:
785
786
787
            self.hl_pos = len(self.highlights) - 1
        elif self.hl_pos > 0:
            self.hl_pos -= 1
788
        log.debug("self.hl_pos = %s" % self.hl_pos)
789
790
791
792
793
794
795
796
        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:
797
                    self.hl_pos = float('nan')
798
799
800
801
802
803
804
                    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

805
    def scroll_up(self, dist=14):
mathieui's avatar
mathieui committed
806
        pos = self.pos
807
        self.pos += dist
808
809
810
811
        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
812
        return self.pos != pos
813
814

    def scroll_down(self, dist=14):
mathieui's avatar
mathieui committed
815
        pos = self.pos
816
817
818
        self.pos -= dist
        if self.pos <= 0:
            self.pos = 0
mathieui's avatar
mathieui committed
819
        return self.pos != pos
820

821
822
823
824
825
826
    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:
827
828
829
            self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1
            if self.pos < 0:
                self.pos = 0
830
831
832
833
834
835
836
837
838
        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)
839

840
841
842
843
    def remove_line_separator(self):
        """
        Remove the line separator
        """
844
        log.debug('remove_line_separator')
845
846
        if None in self.built_lines:
            self.built_lines.remove(None)
847
            self.separator_after = None
848

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

862
    def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False):
863
864
865
866
867
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
868
869
870
871
872
873
        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]:
874
            return 0
875
876
        if highlight:
            self.highlights.append(lines[0])
877
878
879
            self.nb_of_highlights_after_separator += 1
            log.debug("Number of highlights after separator is now %s" % \
                          self.nb_of_highlights_after_separator)
880
881
882
883
884
885
886
887
888
889
890
891
        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]
892
        txt = message.txt
893
        if not txt:
894
895
            return []
        ret = []
louiz’'s avatar
louiz’ committed
896
        nick = truncate_nick(message.nickname)
897
        offset = 0
898
        if nick:
899
            offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length
900
901
        if message.revisions > 0:
            offset += ceil(log10(message.revisions + 1))
902
903
        if message.me:
            offset += 1 # '* ' before and ' ' after
904
905
906
907
908
909
910
        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
911
        lines = poopt.cut_text(txt, self.width-offset-1)
912
913
        prepend = ''
        attrs = []
914
        for line in lines:
915
916
917
918
919
920
            saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend)
            attrs = parse_attrs(message.txt[line[0]:line[1]], attrs)
            if attrs:
                prepend = '\x19' + '\x19'.join(attrs)
            else:
                prepend = ''
921
922
            ret.append(saved)
        return ret
923

924
    def refresh<