window.py 29 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# -*- coding:utf-8 -*-
#
# Copyright 2010 Le Coz Florent <louizatakk@fedoraproject.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Poezio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Poezio.  If not, see <http://www.gnu.org/licenses/>.

19 20
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
21
from os.path import isfile
22 23 24 25

import locale
locale.setlocale(locale.LC_ALL, '')

26
import curses
27
from config import config
28

29 30
from threading import Lock

31
from message import Line
32
import theme
33

34 35
g_lock = Lock()

36 37 38 39 40 41
class Win(object):
    def __init__(self, height, width, y, x, parent_win):
        self._resize(height, width, y, x, parent_win)

    def _resize(self, height, width, y, x, parent_win):
        self.height, self.width, self.x, self.y = height, width, x, y
42 43 44
        try:
            self.win = parent_win.subwin(height, width, y, x)
        except:
45 46 47 48
            # When resizing in a too little height (less than 3 lines)
            # We don't need to resize the window, since this size
            # just makes no sense
            # Just don't crash when this happens.
49 50 51
            # (°>       also, a penguin
            # //\
            # V_/_
52
            pass
53
        self.win.idlok(1)
54
        self.win.leaveok(1)
55

56 57
    def refresh(self):
        self.win.noutrefresh()
58 59

class UserList(Win):
60
    def __init__(self, height, width, y, x, parent_win, visible):
61
        Win.__init__(self, height, width, y, x, parent_win)
62
        self.visible = visible
63 64 65 66
        self.color_role = {'moderator': theme.COLOR_USER_MODERATOR,
                           'participant':theme.COLOR_USER_PARTICIPANT,
                           'visitor':theme.COLOR_USER_VISITOR,
                           'none':theme.COLOR_USER_NONE
67
                           }
68 69 70 71 72
        self.color_show = {'xa':theme.COLOR_STATUS_XA,
                           'None':theme.COLOR_STATUS_NONE,
                           'dnd':theme.COLOR_STATUS_DND,
                           'away':theme.COLOR_STATUS_AWAY,
                           'chat':theme.COLOR_STATUS_CHAT
73
                           }
74 75

    def refresh(self, users):
76
        def compare_user(a, b):
77 78 79
            try:
                arole = self.color_role[a.role]
            except KeyError:
80
                arole = 5
81 82 83
            try:
                brole = self.color_role[b.role]
            except KeyError:
84
                brole = 5
85
            if arole == brole:
86 87 88
                if a.nick.lower() < b.nick.lower():
                    return -1
                return 1
89
            return arole - brole
90 91
        if not self.visible:
            return
92
        g_lock.acquire()
93
        self.win.erase()
94
        y = 0
95
        for user in sorted(users, compare_user):
96
            try:
97
                role_col = self.color_role[user.role]
98
            except KeyError:
99
                role_col = theme.COLOR_USER_NONE
100 101
            try:
                show_col = self.color_show[user.show]
102
            except KeyError:
103
                show_col = theme.COLOR_STATUS_NONE
104
            self.win.attron(curses.color_pair(show_col))
105
            self.win.addnstr(y, 0, theme.CHAR_STATUS, 1)
106 107
            self.win.attroff(curses.color_pair(show_col))
            self.win.attron(curses.color_pair(role_col))
108 109 110 111
            try:
                self.win.addnstr(y, 1, user.nick, self.width-1)
            except:
                pass
112
            self.win.attroff(curses.color_pair(role_col))
113
            y += 1
114 115
            if y == self.height:
                break
116
        self.win.refresh()
117
        g_lock.release()
118

119 120 121 122
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
123 124
        self._resize(height, width, y, x, stdscr)

125
class Topic(Win):
126 127
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
128 129
        Win.__init__(self, height, width, y, x, parent_win)

130
    def resize(self, height, width, y, x, stdscr, visible):
131 132
        self._resize(height, width, y, x, stdscr)

133
    def refresh(self, topic, jid=None):
134 135
        if not self.visible:
            return
136
        g_lock.acquire()
137
        self.win.erase()
138 139
        if not jid:
            try:
140
                self.win.addstr(0, 0, topic, curses.color_pair(theme.COLOR_TOPIC_BAR))
141 142
                while True:
                    try:
143
                        self.win.addch(' ', curses.color_pair(theme.COLOR_TOPIC_BAR))
144 145
                    except:
                        break
146 147 148 149 150 151 152
            except:
                pass
        elif jid:
            room = jid.split('/')[0]
            nick = '/'.join(jid.split('/')[1:])
            topic = _('%(nick)s from room %(room)s' % {'nick': nick, 'room':room})
            self.win.addnstr(0, 0, topic.encode('utf-8') + " "*(self.width-len(topic)), self.width-1
153
                             , curses.color_pair(theme.COLOR_PRIVATE_ROOM_BAR))
154

155
        self.win.refresh()
156
        g_lock.release()
157

158 159 160 161 162 163 164 165
class RoomInfo(Win):
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
        Win.__init__(self, height, width, y, x, parent_win)

    def resize(self, height, width, y, x, stdscr, visible):
        self._resize(height, width, y, x, stdscr)

166 167 168 169 170 171 172
    def print_scroll_position(self, current_room):
        """
        Print, link in Weechat, a -PLUS(n)- where n
        is the number of available lines to scroll
        down
        """
        if current_room.pos > 0:
173
            self.win.addstr(' -PLUS(%s)-' % current_room.pos, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
174

175 176 177 178 179
    def refresh(self, rooms, current):
        if not self.visible:
            return
        def compare_room(a, b):
            return a.nb - b.nb
180
        g_lock.acquire()
181
        self.win.erase()
182
        self.win.addnstr(0, 0, "[", self.width
183
                         ,curses.color_pair(theme.COLOR_INFORMATION_BAR))
184 185
        sorted_rooms = sorted(rooms, compare_room)
        for room in sorted_rooms:
186
            color = room.color_state
187
            try:
188
                self.win.addstr("%s" % str(room.nb), curses.color_pair(color))
189
                self.win.addstr(u"|".encode('utf-8'), curses.color_pair(theme.COLOR_INFORMATION_BAR))
190 191 192 193
            except:             # end of line
                break
        (y, x) = self.win.getyx()
        try:
194
            self.win.addstr(y, x-1, '] '+ current.name, curses.color_pair(theme.COLOR_INFORMATION_BAR))
195
        except:
196
            try:
197
                self.win.addstr(y, x-1, '] '+ current.name.encode('utf-8'), curses.color_pair(theme.COLOR_INFORMATION_BAR))
198 199
            except:
                pass
200
            pass
201
        self.print_scroll_position(current)
202 203
        while True:
            try:
204
                self.win.addstr(' ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
205 206
            except:
                break
207
        self.win.refresh()
208
        g_lock.release()
209

210
class TextWin(Win):
211
    """
212
    Just keep ONE single window for the text area and rewrite EVERYTHING
213
    on each change. (thanks weechat :o)
214
    """
215 216
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
217 218 219 220 221
        self.height = height
        self.width = width
        self.y = y
        self.x = x
        self.parent_win = parent_win
222
        Win.__init__(self, height, width, y, x, parent_win)
223 224
        self.win.scrollok(1)

225
    def build_lines_from_messages(self, messages):
226
        """
227 228
        From all the existing messages in the window, create the that will
        be displayed on the screen
229
        """
230 231
        lines = []
        for message in messages:
232 233 234
            if message == None:  # line separator
                lines.append(None)
                continue
235
            txt = message.txt
236 237
            if not txt:
                continue
238
            offset = 11         # length of the time
239 240 241 242 243 244
            if message.nickname and len(message.nickname) >= 30:
                nick = message.nickname[:30]+u'…'
            else:
                nick = message.nickname
            if nick:
                offset += len(nick) + 2 # + nick + spaces length
245
            first = True
246
            this_line_was_broken_by_space = False
247 248 249 250
            while txt != '':
                if txt[:self.width-offset].find('\n') != -1:
                    limit = txt[:self.width-offset].find('\n')
                else:
251
                    # break between words if possible
252 253
                    if len(txt) >= self.width-offset:
                        limit = txt[:self.width-offset].rfind(' ')
254 255 256 257 258 259 260
                        this_line_was_broken_by_space = True
                        if limit <= 0:
                            limit = self.width-offset-1
                            this_line_was_broken_by_space = False
                    else:
                        limit = self.width-offset-1
                        this_line_was_broken_by_space = False
261
                color = message.user.color if message.user else None
262 263
                if not first:
                    nick = None
264 265 266
                    time = None
                else:
                    time = message.time
267
                l = Line(nick, color,
268
                         time,
269
                         txt[:limit], message.color,
270 271
                         offset,
                         message.colorized)
272
                lines.append(l)
273 274 275 276
                if this_line_was_broken_by_space:
                    txt = txt[limit+1:] # jump the space at the start of the line
                else:
                    txt = txt[limit:]
277 278
                if txt.startswith('\n'):
                    txt = txt[1:]
279
                first = False
280
        return lines
281
        return lines[-len(messages):] # return only the needed number of lines
282

283
    def refresh(self, room):
284
        """
285 286
        Build the Line objects from the messages, and then write
        them in the text area
287
        """
288 289
        if not self.visible:
            return
290
        g_lock.acquire()
291
        self.win.erase()
292
        lines = self.build_lines_from_messages(room.messages)
293 294 295 296
        if room.pos + self.height > len(lines):
            room.pos = len(lines) - self.height
            if room.pos < 0:
                room.pos = 0
297
        if room.pos != 0:
298
            lines = lines[-self.height-room.pos:-room.pos]
299
        else:
300
            lines = lines[-self.height:]
301 302 303
        y = 0
        for line in lines:
            self.win.move(y, 0)
304 305 306 307
            if line == None:
                self.write_line_separator()
                y += 1
                continue
308 309 310 311
            if line.time is not None:
                self.write_time(line.time)
            if line.nickname is not None:
                self.write_nickname(line.nickname.encode('utf-8'), line.nickname_color)
312
            self.write_text(y, line.text_offset, line.text, line.text_color, line.colorized)
313
            y += 1
314
        self.win.refresh()
315
        g_lock.release()
316

317 318 319
    def write_line_separator(self):
        """
        """
320
        self.win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
321
        self.win.addstr(' -'*(self.width/2))
322
        self.win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
323

324
    def write_text(self, y, x, txt, color, colorized):
325
        """
326
        write the text of a line.
327 328
        """
        txt = txt.encode('utf-8')
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
        if not colorized:
            if color:
                self.win.attron(curses.color_pair(color))
            try:
                self.win.addstr(y, x, txt)
            except:  # bug 1665
                pass
            if color:
                self.win.attroff(curses.color_pair(color))

        else:                   # Special messages like join or quit
            from common import debug
            special_words = {
                theme.CHAR_JOIN: theme.COLOR_JOIN_CHAR,
                theme.CHAR_QUIT: theme.COLOR_QUIT_CHAR,
                theme.CHAR_KICK: theme.COLOR_KICK_CHAR,
                }
            for word in txt.split():
                if word in special_words.keys():
                    self.win.attron(curses.color_pair(special_words[word]))
                    self.win.addstr(word)
                    self.win.attroff(curses.color_pair(special_words[word]))
                elif word.startswith('(') and word.endswith(')'):
                    self.win.addstr('(', curses.color_pair(color))
                    self.win.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.win.addstr(')', curses.color_pair(color))
                elif word.startswith('{') and word.endswith('}'):
                    self.win.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
                elif word.startswith('[') and word.endswith(']'):
                    self.win.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
                else:
                    self.win.attron(curses.color_pair(color))
                    self.win.addstr(word)
                    self.win.attroff(curses.color_pair(color))
                try:
                    self.win.addstr(' ')
                except:
                    pass
367

368
    def write_nickname(self, nickname, color):
369 370 371 372
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
373 374
        if color:
            self.win.attron(curses.color_pair(color))
375
        self.win.addstr(nickname)
376 377
        if color:
            self.win.attroff(curses.color_pair(color))
378
        self.win.addnstr("> ", 2)
379

380
    def write_time(self, time):
381 382 383
        """
        Write the date on the yth line of the window
        """
384 385 386 387 388 389 390 391 392
        self.win.attron(curses.color_pair(theme.COLOR_TIME_BRACKETS))
        self.win.addnstr('[', 1)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_BRACKETS))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.win.addnstr(time.strftime("%H"), 2)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_NUMBERS))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_SEPARATOR))
393
        self.win.addnstr(':', 1)
394 395 396 397 398 399 400
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_SEPARATOR))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.win.addnstr(time.strftime("%M"), 2)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_NUMBERS))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_SEPARATOR))
401
        self.win.addnstr(':', 1)
402 403 404 405 406 407 408 409 410 411 412
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_SEPARATOR))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.win.addnstr(time.strftime('%S'), 2)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_NUMBERS))

        self.win.attron(curses.color_pair(theme.COLOR_TIME_BRACKETS))
        self.win.addstr(']')
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_BRACKETS))

        self.win.addstr(' ')
413

414
    def resize(self, height, width, y, x, stdscr, visible):
415
        self.visible = visible
416 417
        self._resize(height, width, y, x, stdscr)

418 419
class Input(Win):
    """
420
    The line where text is entered
421
    """
422
    def __init__(self, height, width, y, x, stdscr, visible):
423
        Win.__init__(self, height, width, y, x, stdscr)
424
        curses.curs_set(1)
425
        self.win.leaveok(0)
426
        self.visible = visible
427 428
        self.history = []
        self.text = u''
429
        self.clipboard = None
430 431 432
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
        # the first char to display on the screen
433
        self.histo_pos = 0
434
        self.hit_list = [] # current possible completion (normal)
435 436
        self.last_completion = None # Contains the last nickname completed,
                                    # if last key was a tab
437

438 439 440 441
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
442
        self._resize(height, width, y, x, stdscr)
443
        self.win.leaveok(0)
444
        self.win.clear()
445
        self.win.addnstr(0, 0, self.text.encode('utf-8'), self.width-1)
446

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 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 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
    def jump_word_left(self):
        """
        Move the cursor one word to the left
        """
        if not len(self.text) or self.pos == 0:
            return
        previous_space = self.text[:self.pos+self.line_pos].rfind(' ')
        if previous_space == -1:
            previous_space = 0
        diff = self.pos+self.line_pos-previous_space
        for i in xrange(diff):
            self.key_left()

    def jump_word_right(self):
        """
        Move the cursor one word to the right
        """
        if len(self.text) == self.pos+self.line_pos or not len(self.text):
            return
        next_space = self.text.find(' ', self.pos+self.line_pos+1)
        if next_space == -1:
            next_space = len(self.text)
        diff = next_space - (self.pos+self.line_pos)
        for i in xrange(diff):
            self.key_right()

    def delete_word(self):
        """
        Delete the word just before the cursor
        """
        if not len(self.text) or self.pos == 0:
            return
        previous_space = self.text[:self.pos+self.line_pos].rfind(' ')
        if previous_space == -1:
            previous_space = 0
        diff = self.pos+self.line_pos-previous_space
        for i in xrange(diff):
            self.key_backspace(False)
        self.rewrite_text()

    def delete_end_of_line(self):
        """
        Cut the text from cursor to the end of line
        """
        if len(self.text) == self.pos+self.line_pos:
            return              # nothing to cut
        self.clipboard = self.text[self.pos+self.line_pos:]
        self.text = self.text[:self.pos+self.line_pos]
        self.key_end()

    def delete_begining_of_line(self):
        """
        Cut the text from cursor to the begining of line
        """
        if self.pos+self.line_pos == 0:
            return
        self.clipboard = self.text[:self.pos+self.line_pos]
        self.text = self.text[self.pos+self.line_pos:]
        self.key_home()

    def paste_clipboard(self):
        """
        Insert what is in the clipboard at the cursor position
        """
        if not self.clipboard or len(self.clipboard) == 0:
            return
        for letter in self.clipboard:
514
            self.do_command(letter.encode('utf-8'))
515

516
    def key_dc(self):
517 518 519
        """
        delete char just after the cursor
        """
520
        self.reset_completion()
521 522 523 524
        if self.pos + self.line_pos == len(self.text):
            return              # end of line, nothing to delete
        self.text = self.text[:self.pos+self.line_pos]+self.text[self.pos+self.line_pos+1:]
        self.rewrite_text()
525

526
    def key_up(self):
527 528 529
        """
        Get the previous line in the history
        """
530 531
        if not len(self.history):
            return
532
        self.win.erase()
533 534 535
        if self.histo_pos >= 0:
            self.histo_pos -= 1
        self.text = self.history[self.histo_pos+1]
536
        self.key_end()
537 538

    def key_down(self):
539 540 541
        """
        Get the next line in the history
        """
542 543
        if not len(self.history):
            return
544
        self.reset_completion()
545
        self.win.erase()
546 547 548
        if self.histo_pos < len(self.history)-1:
            self.histo_pos += 1
            self.text = self.history[self.histo_pos]
549
            self.key_end()
550 551 552 553
        else:
            self.histo_pos = len(self.history)-1
            self.text = u''
            self.pos = 0
554 555
            self.line_pos = 0
            self.rewrite_text()
556 557

    def key_home(self):
558 559 560
        """
        Go to the begining of line
        """
561
        self.reset_completion()
562
        self.pos = 0
563 564
        self.line_pos = 0
        self.rewrite_text()
565

566 567 568 569 570 571
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
572
        if len(self.text) >= self.width-1:
573 574
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
575
        else:
576 577 578
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
579 580

    def key_left(self):
581 582 583
        """
        Move the cursor one char to the left
        """
584
        self.reset_completion()
585
        (y, x) = self.win.getyx()
586 587 588
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
589
            self.pos -= 1
590
        self.rewrite_text()
591 592

    def key_right(self):
593 594 595
        """
        Move the cursor one char to the right
        """
596
        self.reset_completion()
597
        (y, x) = self.win.getyx()
598 599 600 601
        if self.pos == self.width-1:
            if self.line_pos + self.width-1 < len(self.text):
                self.line_pos += 1
        elif self.pos < len(self.text):
602
            self.pos += 1
603
        self.rewrite_text()
604

605
    def key_backspace(self, reset=True):
606 607 608
        """
        Delete the char just before the cursor
        """
609
        self.reset_completion()
610
        (y, x) = self.win.getyx()
611 612 613 614
        if self.pos == 0:
            return
        self.text = self.text[:self.pos+self.line_pos-1]+self.text[self.pos+self.line_pos:]
        self.key_left()
615 616
        if reset:
            self.rewrite_text()
617

618
    def auto_completion(self, user_list):
619 620 621
        """
        Complete the nickname
        """
622
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
623
            return # we don't complete if cursor is not at the end of line
624
        completion_type = config.get('completion', 'normal')
625
        if completion_type == 'shell' and self.text != '':
626 627 628 629 630
            self.shell_completion(user_list)
        else:
            self.normal_completion(user_list)

    def reset_completion(self):
631 632 633
        """
        Reset the completion list (called on ALL keys except tab)
        """
634
        self.hit_list = []
635
        self.last_completion = None
636 637

    def normal_completion(self, user_list):
638 639 640
        """
        Normal completion
        """
641 642
        if " " not in self.text.strip() or\
                self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" ":
643
            after = config.get('after_completion', ',')+" "
644 645 646
            #if " " in self.text.strip() and (not self.last_completion or ' ' in self.last_completion):
        else:
            after = " " # don't put the "," if it's not the begining of the sentence
647
        (y, x) = self.win.getyx()
648
        if not self.last_completion:
649
            # begin is the begining of the nick we want to complete
650
            if self.text.strip() != '':
651 652 653
                begin = self.text.split()[-1].encode('utf-8').lower()
            else:
                begin = ''
654 655
            hit_list = []       # list of matching nicks
            for user in user_list:
656 657
                if user.lower().startswith(begin):
                    hit_list.append(user)
658 659 660
            if len(hit_list) == 0:
                return
            self.hit_list = hit_list
661
            end = len(begin.decode('utf-8'))
662
        else:
663
            begin = self.text[-len(after)-len(self.last_completion):-len(after)]
664
            self.hit_list.append(self.hit_list.pop(0)) # rotate list
665
            end = len(begin.decode('utf-8')) + len(after)
666 667
        self.text = self.text[:-end]
        nick = self.hit_list[0] # take the first hit
668
        self.last_completion = nick.decode('utf-8')
669
        self.text += nick.decode('utf-8') +after
670
        self.key_end(False)
671 672

    def shell_completion(self, user_list):
673 674 675
        """
        Shell-like completion
        """
676
        if " " in self.text.strip():
677 678 679
            after = " " # don't put the "," if it's not the begining of the sentence
        else:
            after = config.get('after_completion', ',')+" "
680
        (y, x) = self.win.getyx()
681 682 683 684
        if self.text != '':
            begin = self.text.split()[-1].encode('utf-8').lower()
        else:
            begin = ''
685 686
        hit_list = []       # list of matching nicks
        for user in user_list:
687 688
            if user.lower().startswith(begin):
                hit_list.append(user)
689 690 691 692
        if len(hit_list) == 0:
            return
        end = False
        nick = ''
693 694
        last_completion = self.last_completion
        self.last_completion = True
695 696
        if len(hit_list) == 1:
            nick = hit_list[0] + after
697 698
            self.last_completion = False
        elif last_completion:
699 700 701
            for n in hit_list:
                if begin.lower() == n.lower():
                    nick = n+after # user DO want this completion (tabbed twice on it)
702
                    self.last_completion = False
703 704 705 706 707 708 709 710 711
        if nick == '':
            while not end and len(nick) < len(hit_list[0]):
                nick = hit_list[0][:len(nick)+1]
                for hit in hit_list:
                    if not hit.lower().startswith(nick.lower()):
                        end = True
                        break
            if end:
                nick = nick[:-1]
712 713
        x -= len(begin.decode('utf-8'))
        self.text = self.text[:-len(begin.decode('utf-8'))]
714
        self.text += nick.decode('utf-8')
715
        self.key_end(False)
716

717
    def do_command(self, key, reset=True):
718
        self.reset_completion()
719
        self.text = self.text[:self.pos+self.line_pos]+key.decode('utf-8')+self.text[self.pos+self.line_pos:]
720
        (y, x) = self.win.getyx()
721
        if x == self.width-1:
722 723 724
            self.line_pos += 1
        else:
            self.pos += 1
725 726
        if reset:
            self.rewrite_text()
727 728

    def get_text(self):
729 730 731
        """
        Clear the input and return the text entered so far
        """
732
        txt = self.text
733 734
        self.text = u''
        self.pos = 0
735
        self.line_pos = 0
736 737 738
        if len(txt) != 0:
            self.history.append(txt)
            self.histo_pos = len(self.history)-1
739
        return txt.encode('utf-8')
740

741 742 743 744 745
    def rewrite_text(self):
        """
        Refresh the line onscreen, from the pos and pos_line
        """
        self.clear_text()
746
        self.win.addstr(self.text[self.line_pos:self.line_pos+self.width-1].encode('utf-8'))
747 748 749
        self.win.move(0, self.pos)
        self.refresh()

750
    def refresh(self):
751 752
        if not self.visible:
            return
753
        self.win.refresh()
754 755

    def clear_text(self):
756
        self.win.erase()
757 758 759 760

class Window(object):
    """
    The whole "screen" that can be seen at once in the terminal.
761
    It contains an userlist, an input zone, a topic zone and a chat zone
762 763 764 765 766 767 768 769 770 771 772
    """
    def __init__(self, stdscr):
        """
        name is the name of the Tab, and it's also
        the JID of the chatroom.
        A particular tab is the "Info" tab which has no
        name (None). This info tab should be unique.
        The stdscr should be passed to know the size of the
        terminal
        """
        self.size = (self.height, self.width) = stdscr.getmaxyx()
773 774 775 776 777
        if self.height < 10 or self.width < 60:
            visible = False
        else:
            visible = True
        if visible:
778
            stdscr.attron(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
779
            stdscr.vline(1, 9*(self.width/10), curses.ACS_VLINE, self.height-2)
780
            stdscr.attroff(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
781
        self.user_win = UserList(self.height-3, (self.width/10)-1, 1, 9*(self.width/10)+1, stdscr, visible)
782 783
        self.topic_win = Topic(1, self.width, 0, 0, stdscr, visible)
        self.info_win = RoomInfo(1, self.width, self.height-2, 0, stdscr, visible)
784 785
        self.text_win = TextWin(self.height-3, (self.width/10)*9, 1, 0, stdscr, visible)
        self.input = Input(1, self.width, self.height-1, 0, stdscr, visible)
786 787 788

    def resize(self, stdscr):
        """
789
        Resize the whole window. i.e. all its sub-windows
790 791
        """
        self.size = (self.height, self.width) = stdscr.getmaxyx()
792
        if self.height < 10 or self.width < 50:
793 794 795 796
            visible = False
        else:
            visible = True
        if visible:
797
            stdscr.attron(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
798
            stdscr.vline(1, 9*(self.width/10), curses.ACS_VLINE, self.height-2)
799
            stdscr.attroff(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
800
        text_width = (self.width/10)*9;
801 802
        self.topic_win.resize(1, self.width, 0, 0, stdscr, visible)
        self.info_win.resize(1, self.width, self.height-2, 0, stdscr, visible)
803 804
        self.text_win.resize(self.height-3, text_width, 1, 0, stdscr, visible)
        self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, stdscr, visible)
805
        self.input.resize(1, self.width, self.height-1, 0, stdscr, visible)
806

807 808 809 810 811
    def refresh(self, rooms):
        """
        'room' is the current one
        """
        room = rooms[0]         # get current room
812
        self.text_win.refresh(room)
813
        self.user_win.refresh(room.users)
814
        self.topic_win.refresh(room.topic, room.jid)
815
        self.info_win.refresh(rooms, room)
816 817 818 819 820
        self.input.refresh()

    def do_command(self, key):
        self.input.do_command(key)
        self.input.refresh()