window.py 33.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 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/>.

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

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

24
import shlex
25
import curses
26
from config import config
27

28 29
from threading import Lock

30
from message import Line
31
import theme
32

33 34
g_lock = Lock()

35 36 37 38 39 40
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
41
        try:
42
            self.win = curses.newwin(height, width, y, x)
43
        except:
44 45 46
            from common import debug
            debug('%s %s %s %s %s\n' % (height, width, y, x, parent_win))
            raise
47 48 49 50
            # 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.
51 52 53
            # (°>       also, a penguin
            # //\
            # V_/_
54
            return
55
        self.win.leaveok(1)
56

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

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
    def addnstr(self, *args):
        """
        addnstr is safe
        """
        try:
            self.win.addnstr(*args)
        except:
            pass

    def addstr(self, *args):
        """
        addstr is not safe
        """
        self.win.addstr(*args)

75
class UserList(Win):
76
    def __init__(self, height, width, y, x, parent_win, visible):
77
        Win.__init__(self, height, width, y, x, parent_win)
78
        self.visible = visible
79 80 81
        self.color_role = {'moderator': theme.COLOR_USER_MODERATOR,
                           'participant':theme.COLOR_USER_PARTICIPANT,
                           'visitor':theme.COLOR_USER_VISITOR,
82 83
                           'none':theme.COLOR_USER_NONE,
                           '':theme.COLOR_USER_NONE
84
                           }
85
        self.color_show = {'xa':theme.COLOR_STATUS_XA,
86 87
                           'none':theme.COLOR_STATUS_NONE,
                           '':theme.COLOR_STATUS_NONE,
88 89 90
                           'dnd':theme.COLOR_STATUS_DND,
                           'away':theme.COLOR_STATUS_AWAY,
                           'chat':theme.COLOR_STATUS_CHAT
91
                           }
92 93

    def refresh(self, users):
94 95
        if not self.visible:
            return
96
        g_lock.acquire()
97
        self.win.erase()
98
        y = 0
99
        for user in sorted(users):
100 101 102 103 104 105 106 107
            if not user.role in self.color_role:
                role_col = theme.COLOR_USER_NONE
            else:
                role_col = self.color_role[user.role]
            if not user.show in self.color_show:
                show_col = theme.COLOR_STATUS_NONE
            else:
                show_col = self.color_show[user.show]
108
            self.addstr(y, 0, theme.CHAR_STATUS, curses.color_pair(show_col))
109
            self.addnstr(y, 1, user.nick, self.width-2, curses.color_pair(role_col))
110
            y += 1
111 112
            if y == self.height:
                break
113
        self.win.refresh()
114
        g_lock.release()
115

116 117 118 119
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
120
        self._resize(height, width, y, x, stdscr)
121 122 123
        self.win.attron(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
        self.win.vline(0, 0, curses.ACS_VLINE, self.height)
        self.win.attroff(curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR))
124

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):
134 135
        if not self.visible:
            return
136
        g_lock.acquire()
137
        self.win.erase()
138 139 140 141 142 143
        self.addnstr(0, 0, topic[:self.width-1], self.width-1, curses.color_pair(theme.COLOR_TOPIC_BAR))
        (y, x) = self.win.getyx()
        remaining_size = self.width - x
        if remaining_size:
            self.addnstr(' '*remaining_size, remaining_size,
                         curses.color_pair(theme.COLOR_INFORMATION_BAR))
144
        self.win.refresh()
145
        g_lock.release()
146

147
class GlobalInfoBar(Win):
148 149 150 151 152 153 154
    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)

155
    def refresh(self, tabs, current):
156 157
        if not self.visible:
            return
158 159 160 161
        def compare_room(a):
            # return a.nb - b.nb
            return a.nb
        comp = lambda x: x.nb
162
        g_lock.acquire()
163
        self.win.erase()
164
        self.addnstr(0, 0, "[", self.width
165
                         ,curses.color_pair(theme.COLOR_INFORMATION_BAR))
166 167 168
        sorted_tabs = sorted(tabs, key=comp)
        for tab in sorted_tabs:
            color = tab.get_color_state()
169
            try:
170
                self.addstr("%s" % str(tab.nb), curses.color_pair(color))
171
                self.addstr("|", curses.color_pair(theme.COLOR_INFORMATION_BAR))
172 173 174
            except:             # end of line
                break
        (y, x) = self.win.getyx()
175
        self.addnstr(y, x-1, '] ', 2, curses.color_pair(theme.COLOR_INFORMATION_BAR))
176 177 178 179
        (y, x) = self.win.getyx()
        remaining_size = self.width - x
        self.addnstr(' '*remaining_size, remaining_size,
                     curses.color_pair(theme.COLOR_INFORMATION_BAR))
180
        self.win.refresh()
181
        g_lock.release()
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
        Win.__init__(self, height, width, y, x, parent_win)

    def print_scroll_position(self, text_buffer):
        """
        Print, link in Weechat, a -PLUS(n)- where n
        is the number of available lines to scroll
        down
        """
        if text_buffer.pos > 0:
            plus = ' -PLUS(%s)-' % text_buffer.pos
            self.addnstr(plus, len(plus), curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)

    def finish_line(self):
        """
        Write colored spaces until the end of line
        """
        (y, x) = self.win.getyx()
        size = self.width-x
        self.addnstr(' '*size, size, curses.color_pair(theme.COLOR_INFORMATION_BAR))

class PrivateInfoWin(InfoWin):
    """
    The live above the information window, displaying informations
    about the MUC user we are talking to
    """
    def __init__(self, height, width, y, x, parent_win, visible):
        InfoWin.__init__(self, height, width, y, x, parent_win, visible)

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

    def refresh(self, room):
        if not self.visible:
            return
        g_lock.acquire()
        self.win.erase()
        self.write_room_name(room)
        self.print_scroll_position(room)
        self.finish_line()
        self.win.refresh()
        g_lock.release()

    def write_room_name(self, room):
        (room_name, nick) = room.name.split('/', 1)
        self.addnstr(nick, len(nick), curses.color_pair(13))
        txt = ' from room %s' % room_name
        self.addnstr(txt, len(txt), curses.color_pair(theme.COLOR_INFORMATION_BAR))

class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
    def __init__(self, height, width, y, x, parent_win, visible):
        InfoWin.__init__(self, height, width, y, x, parent_win, visible)

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

    def refresh(self, room):
        if not self.visible:
            return
        g_lock.acquire()
        self.win.erase()
        self.write_room_name(room)
        self.write_own_nick(room)
        self.write_disconnected(room)
        self.write_role(room)
        self.print_scroll_position(room)
        self.finish_line()
        self.win.refresh()
        g_lock.release()

    def write_room_name(self, room):
        """
        """
        self.addnstr('[', 1, curses.color_pair(theme.COLOR_INFORMATION_BAR))
        self.addnstr(room.name, len(room.name), curses.color_pair(13))
        self.addnstr('] ', 2, curses.color_pair(theme.COLOR_INFORMATION_BAR))

    def write_disconnected(self, room):
        """
        Shows a message if the room is not joined
        """
        if not room.joined:
            self.addnstr(' -!- Not connected ', 21, curses.color_pair(theme.COLOR_INFORMATION_BAR))
    def write_own_nick(self, room):
        """
        Write our own nick in the info bar
        """
        nick = room.own_nick
        if not nick:
            return
        if len(nick) > 13:
            nick = nick[:13]+'…'
            length = 14
        else:
            length = len(nick)
        self.addnstr(nick, length, curses.color_pair(theme.COLOR_INFORMATION_BAR))

    def write_role(self, room):
        """
        Write our own role and affiliation
        """
        from common import debug

        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+')'
        self.addnstr(txt, len(txt), curses.color_pair(theme.COLOR_INFORMATION_BAR))

309
class TextWin(Win):
310
    """
311
    Just keep ONE single window for the text area and rewrite EVERYTHING
312
    on each change. (thanks weechat :o)
313
    """
314
    def __init__(self, height, width, y, x, parent_win, visible):
315
        Win.__init__(self, height, width, y, x, parent_win)
316
        self.visible = visible
317

318
    def build_lines_from_messages(self, messages):
319
        """
320 321
        From all the existing messages in the window, create the that will
        be displayed on the screen
322
        """
323 324
        lines = []
        for message in messages:
325 326 327
            if message == None:  # line separator
                lines.append(None)
                continue
328
            txt = message.txt
329 330
            if not txt:
                continue
331 332
            # length of the time
            offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1])
333
            if message.nickname and len(message.nickname) >= 30:
334
                nick = message.nickname[:30]+'…'
335 336 337 338
            else:
                nick = message.nickname
            if nick:
                offset += len(nick) + 2 # + nick + spaces length
339
            first = True
340
            this_line_was_broken_by_space = False
341 342 343 344
            while txt != '':
                if txt[:self.width-offset].find('\n') != -1:
                    limit = txt[:self.width-offset].find('\n')
                else:
345
                    # break between words if possible
346 347
                    if len(txt) >= self.width-offset:
                        limit = txt[:self.width-offset].rfind(' ')
348 349
                        this_line_was_broken_by_space = True
                        if limit <= 0:
350
                            limit = self.width-offset
351 352 353 354
                            this_line_was_broken_by_space = False
                    else:
                        limit = self.width-offset-1
                        this_line_was_broken_by_space = False
355
                color = message.user.color if message.user else None
356 357
                if not first:
                    nick = None
358 359 360
                    time = None
                else:
                    time = message.time
361
                l = Line(nick, color,
362
                         time,
363
                         txt[:limit], message.color,
364 365
                         offset,
                         message.colorized)
366
                lines.append(l)
367 368 369 370
                if this_line_was_broken_by_space:
                    txt = txt[limit+1:] # jump the space at the start of the line
                else:
                    txt = txt[limit:]
371 372
                if txt.startswith('\n'):
                    txt = txt[1:]
373
                first = False
374
        return lines
375
        return lines[-len(messages):] # return only the needed number of lines
376

377
    def refresh(self, room):
378
        """
379 380
        Build the Line objects from the messages, and then write
        them in the text area
381
        """
382 383
        if not self.visible:
            return
384 385
        if self.height <= 0:
            return
386
        g_lock.acquire()
387
        self.win.erase()
388
        lines = self.build_lines_from_messages(room.messages)
389 390 391 392
        if room.pos + self.height > len(lines):
            room.pos = len(lines) - self.height
            if room.pos < 0:
                room.pos = 0
393
        if room.pos != 0:
394
            lines = lines[-self.height-room.pos:-room.pos]
395
        else:
396
            lines = lines[-self.height:]
397 398 399
        y = 0
        for line in lines:
            self.win.move(y, 0)
400 401 402 403
            if line == None:
                self.write_line_separator()
                y += 1
                continue
404 405 406
            if line.time is not None:
                self.write_time(line.time)
            if line.nickname is not None:
407
                self.write_nickname(line.nickname, line.nickname_color)
408
            self.write_text(y, line.text_offset, line.text, line.text_color, line.colorized)
409
            y += 1
410
        self.win.refresh()
411
        g_lock.release()
412

413 414 415
    def write_line_separator(self):
        """
        """
416
        self.win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
417
        self.addstr(' -'*(self.width//2-1))
418
        self.win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
419

420
    def write_text(self, y, x, txt, color, colorized):
421
        """
422
        write the text of a line.
423
        """
424
        txt = txt
425 426 427
        if not colorized:
            if color:
                self.win.attron(curses.color_pair(color))
428
            self.addstr(y, x, txt)
429 430 431 432 433 434 435 436 437 438
            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,
                }
439 440 441
            try:
                splitted = shlex.split(txt)
            except ValueError:
442
                txt = txt.replace('"', '')
443 444
                splitted = shlex.split(txt)
            for word in splitted:
445
                if word in list(special_words.keys()):
446
                    self.addstr(word, curses.color_pair(special_words[word]))
447
                elif word.startswith('(') and word.endswith(')'):
448 449 450
                    self.addstr('(', curses.color_pair(color))
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.addstr(')', curses.color_pair(color))
451
                elif word.startswith('{') and word.endswith('}'):
452
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
453
                elif word.startswith('[') and word.endswith(']'):
454
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
455
                else:
456 457
                    self.addstr(word, curses.color_pair(color))
                self.addnstr(' ', 1)
458

459
    def write_nickname(self, nickname, color):
460 461 462 463
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
464 465
        if color:
            self.win.attron(curses.color_pair(color))
466
        self.addstr(nickname)
467 468
        if color:
            self.win.attroff(curses.color_pair(color))
469
        self.addnstr("> ", 2)
470

471
    def write_time(self, time):
472 473 474
        """
        Write the date on the yth line of the window
        """
475 476 477
        self.win.attron(curses.color_pair(theme.COLOR_TIME_LIMITER))
        self.addnstr(theme.CHAR_TIME_LEFT, 1)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_LIMITER))
478 479

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

        self.win.attron(curses.color_pair(theme.COLOR_TIME_SEPARATOR))
484
        self.addnstr(':', 1)
485 486 487
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_SEPARATOR))

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

        self.win.attron(curses.color_pair(theme.COLOR_TIME_SEPARATOR))
492
        self.addnstr(':', 1)
493 494 495
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_SEPARATOR))

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

499 500 501
        self.win.attron(curses.color_pair(theme.COLOR_TIME_LIMITER))
        self.addnstr(theme.CHAR_TIME_RIGHT, 1)
        self.win.attroff(curses.color_pair(theme.COLOR_TIME_LIMITER))
502

503
        self.addstr(' ')
504

505
    def resize(self, height, width, y, x, stdscr, visible):
506
        self.visible = visible
507 508
        self._resize(height, width, y, x, stdscr)

509 510
class Input(Win):
    """
511
    The line where text is entered
512
    """
513
    def __init__(self, height, width, y, x, stdscr, visible):
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
        self.key_func = {
            "KEY_LEFT": self.key_left,
            "M-D": self.key_left,
            "KEY_RIGHT": self.key_right,
            "M-C": self.key_right,
            "KEY_UP": self.key_up,
            "M-A": self.key_up,
            "KEY_END": self.key_end,
            "KEY_HOME": self.key_home,
            "KEY_DOWN": self.key_down,
            "M-B": self.key_down,
            "KEY_DC": self.key_dc,
            '^D': self.key_dc,
            'M-b': self.jump_word_left,
            '^W': self.delete_word,
            '^K': self.delete_end_of_line,
            '^U': self.delete_begining_of_line,
            '^Y': self.paste_clipboard,
            '^A': self.key_home,
            '^E': self.key_end,
            'M-f': self.jump_word_right,
            "KEY_BACKSPACE": self.key_backspace,
            '^?': self.key_backspace,
            '^J': self.get_text,
            '\n': self.get_text,
            }

541
        Win.__init__(self, height, width, y, x, stdscr)
542
        self.visible = visible
543
        self.history = []
544
        self.text = ''
545
        self.clipboard = None
546 547 548
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
        # the first char to display on the screen
549
        self.histo_pos = 0
550
        self.hit_list = [] # current possible completion (normal)
551 552
        self.last_completion = None # Contains the last nickname completed,
                                    # if last key was a tab
553

554 555 556 557
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
558 559
        self._resize(height, width, y, x, stdscr)
        self.win.clear()
560
        self.addnstr(0, 0, self.text, self.width-1)
561

562 563 564 565 566 567 568 569 570 571
    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
572
        for i in range(diff):
573 574 575 576 577 578 579 580 581 582 583 584
            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)
585
        for i in range(diff):
586 587 588 589 590 591 592 593 594 595 596 597
            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
598
        for i in range(diff):
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
            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:
629
            self.do_command(letter)
630

631
    def key_dc(self):
632 633 634
        """
        delete char just after the cursor
        """
635
        self.reset_completion()
636 637 638 639
        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()
640

641
    def key_up(self):
642 643 644
        """
        Get the previous line in the history
        """
645 646
        if not len(self.history):
            return
647
        self.win.erase()
648 649 650
        if self.histo_pos >= 0:
            self.histo_pos -= 1
        self.text = self.history[self.histo_pos+1]
651
        self.key_end()
652 653

    def key_down(self):
654 655 656
        """
        Get the next line in the history
        """
657 658
        if not len(self.history):
            return
659
        self.reset_completion()
660 661 662
        if self.histo_pos < len(self.history)-1:
            self.histo_pos += 1
            self.text = self.history[self.histo_pos]
663
            self.key_end()
664 665
        else:
            self.histo_pos = len(self.history)-1
666
            self.text = ''
667
            self.pos = 0
668 669
            self.line_pos = 0
            self.rewrite_text()
670 671

    def key_home(self):
672 673 674
        """
        Go to the begining of line
        """
675
        self.reset_completion()
676
        self.pos = 0
677 678
        self.line_pos = 0
        self.rewrite_text()
679

680 681 682 683 684 685
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
686
        if len(self.text) >= self.width-1:
687 688
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
689
        else:
690 691 692
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
693 694

    def key_left(self):
695 696 697
        """
        Move the cursor one char to the left
        """
698
        self.reset_completion()
699
        (y, x) = self.win.getyx()
700 701 702
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
703
            self.pos -= 1
704
        self.rewrite_text()
705 706

    def key_right(self):
707 708 709
        """
        Move the cursor one char to the right
        """
710
        self.reset_completion()
711
        (y, x) = self.win.getyx()
712 713 714 715
        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):
716
            self.pos += 1
717
        self.rewrite_text()
718

719
    def key_backspace(self, reset=True):
720 721 722
        """
        Delete the char just before the cursor
        """
723
        self.reset_completion()
724
        (y, x) = self.win.getyx()
725 726 727 728
        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()
729 730
        if reset:
            self.rewrite_text()
731

732
    def auto_completion(self, user_list, add_after=True):
733 734 735
        """
        Complete the nickname
        """
736
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
737
            return # we don't complete if cursor is not at the end of line
738
        completion_type = config.get('completion', 'normal')
739
        if completion_type == 'shell' and self.text != '':
740
            self.shell_completion(user_list, add_after)
741
        else:
742
            self.normal_completion(user_list, add_after)
743 744

    def reset_completion(self):
745 746 747
        """
        Reset the completion list (called on ALL keys except tab)
        """
748
        self.hit_list = []
749
        self.last_completion = None
750

751
    def normal_completion(self, user_list, add_after):
752 753 754
        """
        Normal completion
        """
755 756
        if add_after and (" " not in self.text.strip() or\
                self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "):
757
            after = config.get('after_completion', ',')+" "
758 759 760
            #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
761
        (y, x) = self.win.getyx()
762
        if not self.last_completion:
763
            # begin is the begining of the nick we want to complete
764
            if self.text.strip() != '':
765
                begin = self.text.split()[-1].lower()
766 767
            else:
                begin = ''
768 769
            hit_list = []       # list of matching nicks
            for user in user_list:
770 771
                if user.lower().startswith(begin):
                    hit_list.append(user)
772 773 774
            if len(hit_list) == 0:
                return
            self.hit_list = hit_list
775
            end = len(begin)
776
        else:
777
            begin = self.text[-len(after)-len(self.last_completion):-len(after)]
778
            self.hit_list.append(self.hit_list.pop(0)) # rotate list
779
            end = len(begin) + len(after)
780 781
        self.text = self.text[:-end]
        nick = self.hit_list[0] # take the first hit
782 783
        self.last_completion = nick
        self.text += nick +after
784
        self.key_end(False)
785

786
    def shell_completion(self, user_list, add_after):
787 788 789
        """
        Shell-like completion
        """
790
        if " " in self.text.strip() or not add_after:
791 792 793
            after = " " # don't put the "," if it's not the begining of the sentence
        else:
            after = config.get('after_completion', ',')+" "
794
        (y, x) = self.win.getyx()
795
        if self.text != '':
796
            begin = self.text.split()[-1].lower()
797 798
        else:
            begin = ''
799 800
        hit_list = []       # list of matching nicks
        for user in user_list:
801 802
            if user.lower().startswith(begin):
                hit_list.append(user)
803 804 805 806
        if len(hit_list) == 0:
            return
        end = False
        nick = ''
807 808
        last_completion = self.last_completion
        self.last_completion = True
809 810
        if len(hit_list) == 1:
            nick = hit_list[0] + after
811 812
            self.last_completion = False
        elif last_completion:
813 814 815
            for n in hit_list:
                if begin.lower() == n.lower():
                    nick = n+after # user DO want this completion (tabbed twice on it)
816
                    self.last_completion = False
817 818 819 820 821 822 823 824 825
        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]
826 827 828
        x -= len(begin)
        self.text = self.text[:-len(begin)]
        self.text += nick
829
        self.key_end(False)
830

831
    def do_command(self, key, reset=True):
832 833 834 835
        if key in self.key_func:
            return self.key_func[key]()
        # if not key or len(key) > 1:
        #     return    # ignore non-handled keyboard shortcuts
836
        self.reset_completion()
837
        self.text = self.text[:self.pos+self.line_pos]+key+self.text[self.pos+self.line_pos:]
838
        (y, x) = self.win.getyx()
839
        if x == self.width-1:
840 841 842
            self.line_pos += 1
        else:
            self.pos += 1
843 844
        if reset:
            self.rewrite_text()
845 846

    def get_text(self):
847 848 849
        """
        Clear the input and return the text entered so far
        """
850
        txt = self.text
851
        self.text = ''
852
        self.pos = 0
853
        self.line_pos = 0
854 855 856
        if len(txt) != 0:
            self.history.append(txt)
            self.histo_pos = len(self.history)-1
857
        self.rewrite_text()
858
        return txt
859

860 861 862 863
    def rewrite_text(self):
        """
        Refresh the line onscreen, from the pos and pos_line
        """
864
        g_lock.acquire()
865
        self.clear_text()
866
        self.addstr(self.text[self.line_pos:self.line_pos+self.width-1])
867 868
        self.win.chgat(0, self.pos, 1, curses.