window.py 40.1 KB
Newer Older
1
# Copyright 2010 Le Coz Florent <louiz@louiz.org>
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#
# 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 contact import Contact, Resource
31 32
from roster import RosterGroup

33
from message import Line
34 35
from tab import MIN_WIDTH, MIN_HEIGHT

36
import theme
37

38 39
g_lock = Lock()

40 41
class Win(object):
    def __init__(self, height, width, y, x, parent_win):
42
        self._resize(height, width, y, x, parent_win, True)
43

44 45 46
    def _resize(self, height, width, y, x, parent_win, visible):
        if not visible:
            return
47
        self.height, self.width, self.x, self.y = height, width, x, y
48 49 50 51 52 53 54 55 56 57 58 59 60 61
        # try:
        self._win = curses.newwin(height, width, y, x)
        # except:
        #     # 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.
        #     # (°>       also, a penguin
        #     # //\
        #     # V_/_
        #     return

    def _refresh(self):
        self._win.noutrefresh()
62

63 64
    def addnstr(self, *args):
        """
65
        Safe call to addnstr
66 67
        """
        try:
68
            self._win.addnstr(*args)
69 70 71 72 73
        except:
            pass

    def addstr(self, *args):
        """
74
        Safe call to addstr
75
        """
76
        try:
77
            self._win.addstr(*args)
78 79
        except:
            pass
80

81 82 83 84
    def finish_line(self, color):
        """
        Write colored spaces until the end of line
        """
85
        (y, x) = self._win.getyx()
86 87 88
        size = self.width-x
        self.addnstr(' '*size, size, curses.color_pair(color))

89
class UserList(Win):
90
    def __init__(self, height, width, y, x, parent_win, visible):
91
        Win.__init__(self, height, width, y, x, parent_win)
92
        self.visible = visible
93 94 95
        self.color_role = {'moderator': theme.COLOR_USER_MODERATOR,
                           'participant':theme.COLOR_USER_PARTICIPANT,
                           'visitor':theme.COLOR_USER_VISITOR,
96 97
                           'none':theme.COLOR_USER_NONE,
                           '':theme.COLOR_USER_NONE
98
                           }
99
        self.color_show = {'xa':theme.COLOR_STATUS_XA,
100 101
                           'none':theme.COLOR_STATUS_NONE,
                           '':theme.COLOR_STATUS_NONE,
102 103 104
                           'dnd':theme.COLOR_STATUS_DND,
                           'away':theme.COLOR_STATUS_AWAY,
                           'chat':theme.COLOR_STATUS_CHAT
105
                           }
106 107

    def refresh(self, users):
108 109
        if not self.visible:
            return
110
        with g_lock:
111
            self._win.erase()
112 113 114 115 116 117 118 119 120 121 122
            y = 0
            for user in sorted(users):
                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]
                self.addstr(y, 0, theme.CHAR_STATUS, curses.color_pair(show_col))
123
                self.addnstr(y, 1, user.nick, self.width-1, curses.color_pair(role_col))
124 125 126
                y += 1
                if y == self.height:
                    break
127
            self._refresh()
128

129 130 131 132
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
133
        self._resize(height, width, y, x, stdscr, visible)
134 135 136
        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))
137

138
class Topic(Win):
139 140
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
141 142
        Win.__init__(self, height, width, y, x, parent_win)

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

146
    def refresh(self, topic):
147 148
        if not self.visible:
            return
149
        with g_lock:
150
            self._win.erase()
151
            self.addstr(0, 0, topic[:self.width-1], curses.color_pair(theme.COLOR_TOPIC_BAR))
152
            (y, x) = self._win.getyx()
153 154 155 156
            remaining_size = self.width - x
            if remaining_size:
                self.addnstr(' '*remaining_size, remaining_size,
                             curses.color_pair(theme.COLOR_INFORMATION_BAR))
157
            self._refresh()
158

159
class GlobalInfoBar(Win):
160 161 162 163 164
    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):
165
        self._resize(height, width, y, x, stdscr, visible)
166

167
    def refresh(self, tabs, current):
168 169
        if not self.visible:
            return
170 171 172 173
        def compare_room(a):
            # return a.nb - b.nb
            return a.nb
        comp = lambda x: x.nb
174
        with g_lock:
175
            self._win.erase()
176
            self.addstr(0, 0, "[", curses.color_pair(theme.COLOR_INFORMATION_BAR))
177 178 179 180 181 182 183 184
            sorted_tabs = sorted(tabs, key=comp)
            for tab in sorted_tabs:
                color = tab.get_color_state()
                try:
                    self.addstr("%s" % str(tab.nb), curses.color_pair(color))
                    self.addstr("|", curses.color_pair(theme.COLOR_INFORMATION_BAR))
                except:             # end of line
                    break
185
            (y, x) = self._win.getyx()
186
            self.addstr(y, x-1, '] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
187
            (y, x) = self._win.getyx()
188 189 190
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
                         curses.color_pair(theme.COLOR_INFORMATION_BAR))
191
            self._refresh()
192

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
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
210
            self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
211 212 213 214 215 216 217 218 219 220

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):
221
        self._resize(height, width, y, x, stdscr, visible)
222 223 224 225

    def refresh(self, room):
        if not self.visible:
            return
226
        with g_lock:
227
            self._win.erase()
228 229 230
            self.write_room_name(room)
            self.print_scroll_position(room)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
231
            self._refresh()
232 233 234

    def write_room_name(self, room):
        (room_name, nick) = room.name.split('/', 1)
235
        self.addstr(nick, curses.color_pair(13))
236
        txt = ' from room %s' % room_name
237
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
238

239 240 241 242 243 244 245 246 247 248 249
class ConversationInfoWin(InfoWin):
    """
    The line 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, visible)

250
    def refresh(self, room, contact):
251 252
        if not self.visible:
            return
253 254 255
        # 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.
256 257
        # Also, contact can be a resource, if we're talking to a
        # specific resource.
258
        with g_lock:
259
            self._win.erase()
260 261 262
            # self.write_room_name(resource, room)
            # self.print_scroll_position(room)
            # self.finish_line(theme.COLOR_INFORMATION_BAR)
263
            self._refresh()
264

265 266 267 268 269
    def write_room_name(self, contact, room):
        if not contact:
            txt = '%s' % room.name
        else:
            txt = '%s' % contact.get_jid().bare
270
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
271

272 273 274 275 276 277 278 279 280
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):
281
        self._resize(height, width, y, x, stdscr, visible)
282 283 284 285

    def refresh(self, room):
        if not self.visible:
            return
286
        with g_lock:
287
            self._win.erase()
288 289 290 291 292 293
            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(theme.COLOR_INFORMATION_BAR)
294
            self._refresh()
295 296 297 298

    def write_room_name(self, room):
        """
        """
299
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
300
        self.addnstr(room.name, len(room.name), curses.color_pair(13))
301
        self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
302 303 304 305 306 307

    def write_disconnected(self, room):
        """
        Shows a message if the room is not joined
        """
        if not room.joined:
308 309
            self.addstr(' -!- Not connected ', curses.color_pair(theme.COLOR_INFORMATION_BAR))

310 311 312 313 314 315 316 317 318
    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]+'…'
319
        self.addstr(nick, curses.color_pair(theme.COLOR_INFORMATION_BAR))
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335

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

338
class TextWin(Win):
339
    """
340
    Just keep ONE single window for the text area and rewrite EVERYTHING
341
    on each change. (thanks weechat :o)
342
    """
343
    def __init__(self, height, width, y, x, parent_win, visible):
344
        Win.__init__(self, height, width, y, x, parent_win)
345
        self.visible = visible
346

347
    def build_lines_from_messages(self, messages):
348
        """
349 350
        From all the existing messages in the window, create the that will
        be displayed on the screen
351
        """
352 353
        lines = []
        for message in messages:
354 355 356
            if message == None:  # line separator
                lines.append(None)
                continue
357
            txt = message.txt
358 359
            if not txt:
                continue
360 361
            # length of the time
            offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1])
362
            if message.nickname and len(message.nickname) >= 30:
363
                nick = message.nickname[:30]+'…'
364 365 366 367
            else:
                nick = message.nickname
            if nick:
                offset += len(nick) + 2 # + nick + spaces length
368
            first = True
369
            this_line_was_broken_by_space = False
370 371 372 373
            while txt != '':
                if txt[:self.width-offset].find('\n') != -1:
                    limit = txt[:self.width-offset].find('\n')
                else:
374
                    # break between words if possible
375 376
                    if len(txt) >= self.width-offset:
                        limit = txt[:self.width-offset].rfind(' ')
377 378
                        this_line_was_broken_by_space = True
                        if limit <= 0:
379
                            limit = self.width-offset
380 381 382 383
                            this_line_was_broken_by_space = False
                    else:
                        limit = self.width-offset-1
                        this_line_was_broken_by_space = False
384
                color = message.user.color if message.user else None
385 386
                if not first:
                    nick = None
387 388 389
                    time = None
                else:
                    time = message.time
390
                l = Line(nick, color,
391
                         time,
392
                         txt[:limit], message.color,
393 394
                         offset,
                         message.colorized)
395
                lines.append(l)
396 397 398 399
                if this_line_was_broken_by_space:
                    txt = txt[limit+1:] # jump the space at the start of the line
                else:
                    txt = txt[limit:]
400 401
                if txt.startswith('\n'):
                    txt = txt[1:]
402
                first = False
403
        return lines
404
        return lines[-len(messages):] # return only the needed number of lines
405

406
    def refresh(self, room):
407
        """
408 409
        Build the Line objects from the messages, and then write
        them in the text area
410
        """
411 412
        if not self.visible:
            return
413 414
        if self.height <= 0:
            return
415
        with g_lock:
416
            self._win.erase()
417 418 419 420 421 422 423 424 425 426 427
            lines = self.build_lines_from_messages(room.messages)
            if room.pos + self.height > len(lines):
                room.pos = len(lines) - self.height
                if room.pos < 0:
                    room.pos = 0
            if room.pos != 0:
                lines = lines[-self.height-room.pos:-room.pos]
            else:
                lines = lines[-self.height:]
            y = 0
            for line in lines:
428
                self._win.move(y, 0)
429 430 431 432 433 434 435 436 437
                if line == None:
                    self.write_line_separator()
                    y += 1
                    continue
                if line.time is not None:
                    self.write_time(line.time)
                if line.nickname is not None:
                    self.write_nickname(line.nickname, line.nickname_color)
                self.write_text(y, line.text_offset, line.text, line.text_color, line.colorized)
438
                y += 1
439
            self._refresh()
440

441 442 443
    def write_line_separator(self):
        """
        """
444
        self._win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
445
        self.addnstr('- '*(self.width//2), self.width)
446
        self._win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
447

448
    def write_text(self, y, x, txt, color, colorized):
449
        """
450
        write the text of a line.
451
        """
452
        txt = txt
453 454
        if not colorized:
            if color:
455
                self._win.attron(curses.color_pair(color))
456
            self.addstr(y, x, txt)
457
            if color:
458
                self._win.attroff(curses.color_pair(color))
459 460 461 462 463 464 465

        else:                   # Special messages like join or quit
            special_words = {
                theme.CHAR_JOIN: theme.COLOR_JOIN_CHAR,
                theme.CHAR_QUIT: theme.COLOR_QUIT_CHAR,
                theme.CHAR_KICK: theme.COLOR_KICK_CHAR,
                }
466 467 468
            try:
                splitted = shlex.split(txt)
            except ValueError:
469 470 471 472 473
                # FIXME colors are disabled on too long words
                txt = txt.replace('"[', '').replace(']"', '')\
                    .replace('"{', '').replace('}"', '')\
                    .replace('"(', '').replace(')"', '')
                splitted = txt.split()
474
            for word in splitted:
475
                if word in list(special_words.keys()):
476
                    self.addstr(word, curses.color_pair(special_words[word]))
477
                elif word.startswith('(') and word.endswith(')'):
478 479 480
                    self.addstr('(', curses.color_pair(color))
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.addstr(')', curses.color_pair(color))
481
                elif word.startswith('{') and word.endswith('}'):
482
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
483
                elif word.startswith('[') and word.endswith(']'):
484
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
485
                else:
486
                    self.addstr(word, curses.color_pair(color))
487
                self._win.addch(' ')
488

489
    def write_nickname(self, nickname, color):
490 491 492 493
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
494
        if color:
495
            self._win.attron(curses.color_pair(color))
496
        self.addstr(nickname)
497
        if color:
498
            self._win.attroff(curses.color_pair(color))
499
        self.addstr("> ")
500

501
    def write_time(self, time):
502 503 504
        """
        Write the date on the yth line of the window
        """
505 506 507 508 509 510 511
        self.addstr(theme.CHAR_TIME_LEFT, curses.color_pair(theme.COLOR_TIME_LIMITER))
        self.addstr(time.strftime("%H"), curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.addstr(':', curses.color_pair(theme.COLOR_TIME_SEPARATOR))
        self.addstr(time.strftime("%M"), curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.addstr(':', curses.color_pair(theme.COLOR_TIME_SEPARATOR))
        self.addstr(time.strftime('%S'), curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.addnstr(theme.CHAR_TIME_RIGHT, curses.color_pair(theme.COLOR_TIME_LIMITER))
512
        self.addstr(' ')
513

514
    def resize(self, height, width, y, x, stdscr, visible):
515
        self.visible = visible
516
        self._resize(height, width, y, x, stdscr, visible)
517

518 519
class Input(Win):
    """
520
    The line where text is entered
521 522 523
    It can be in input mode or in commmand mode.
    Command mode means that single_key_commands can be entered, handled
    by the Tab object, while this input just displays an help text.
524
    """
525
    def __init__(self, height, width, y, x, stdscr, visible, input_mode=True, help_text=''):
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
        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,
            }

553
        Win.__init__(self, height, width, y, x, stdscr)
554 555
        self.input_mode = input_mode
        self.help_text = help_text # the text displayed in command_mode
556
        self.visible = visible
557
        self.history = []
558
        self.text = ''
559
        self.clipboard = None
560 561 562
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
        # the first char to display on the screen
563
        self.histo_pos = 0
564
        self.hit_list = [] # current possible completion (normal)
565 566
        self.last_completion = None # Contains the last nickname completed,
                                    # if last key was a tab
567

568 569 570
    def is_empty(self):
        return len(self.text) == 0

571 572 573 574
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
575
        self._resize(height, width, y, x, stdscr, visible)
576
        self._win.erase()
577
        self.addnstr(0, 0, self.text, self.width-1)
578

579 580 581 582 583 584 585 586 587 588
    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
589
        for i in range(diff):
590 591 592 593 594 595 596 597 598 599 600 601
            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)
602
        for i in range(diff):
603 604 605 606 607 608 609 610 611 612 613 614
            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
615
        for i in range(diff):
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
            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:
646
            self.do_command(letter)
647

648
    def key_dc(self):
649 650 651
        """
        delete char just after the cursor
        """
652
        self.reset_completion()
653 654 655 656
        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()
657

658
    def key_up(self):
659 660 661
        """
        Get the previous line in the history
        """
662 663
        if not len(self.history):
            return
664
        self._win.erase()
665 666 667
        if self.histo_pos >= 0:
            self.histo_pos -= 1
        self.text = self.history[self.histo_pos+1]
668
        self.key_end()
669 670

    def key_down(self):
671 672 673
        """
        Get the next line in the history
        """
674 675
        if not len(self.history):
            return
676
        self.reset_completion()
677 678 679
        if self.histo_pos < len(self.history)-1:
            self.histo_pos += 1
            self.text = self.history[self.histo_pos]
680
            self.key_end()
681 682
        else:
            self.histo_pos = len(self.history)-1
683
            self.text = ''
684
            self.pos = 0
685 686
            self.line_pos = 0
            self.rewrite_text()
687 688

    def key_home(self):
689 690 691
        """
        Go to the begining of line
        """
692
        self.reset_completion()
693
        self.pos = 0
694 695
        self.line_pos = 0
        self.rewrite_text()
696

697 698 699 700 701 702
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
703
        if len(self.text) >= self.width-1:
704 705
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
706
        else:
707 708 709
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
710 711

    def key_left(self):
712 713 714
        """
        Move the cursor one char to the left
        """
715
        self.reset_completion()
716
        (y, x) = self._win.getyx()
717 718 719
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
720
            self.pos -= 1
721
        self.rewrite_text()
722 723

    def key_right(self):
724 725 726
        """
        Move the cursor one char to the right
        """
727
        self.reset_completion()
728
        (y, x) = self._win.getyx()
729 730 731 732
        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):
733
            self.pos += 1
734
        self.rewrite_text()
735

736
    def key_backspace(self, reset=True):
737 738 739
        """
        Delete the char just before the cursor
        """
740
        self.reset_completion()
741
        (y, x) = self._win.getyx()
742 743 744 745
        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()
746 747
        if reset:
            self.rewrite_text()
748

749
    def auto_completion(self, user_list, add_after=True):
750 751 752
        """
        Complete the nickname
        """
753
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
754
            return # we don't complete if cursor is not at the end of line
755
        completion_type = config.get('completion', 'normal')
756
        if completion_type == 'shell' and self.text != '':
757
            self.shell_completion(user_list, add_after)
758
        else:
759
            self.normal_completion(user_list, add_after)
760 761

    def reset_completion(self):
762 763 764
        """
        Reset the completion list (called on ALL keys except tab)
        """
765
        self.hit_list = []
766
        self.last_completion = None
767

768
    def normal_completion(self, user_list, add_after):
769 770 771
        """
        Normal completion
        """
772 773
        if add_after and (" " not in self.text.strip() or\
                self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "):
774
            after = config.get('after_completion', ',')+" "
775 776 777
            #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
778
        (y, x) = self._win.getyx()
779
        if not self.last_completion:
780
            # begin is the begining of the nick we want to complete
781
            if self.text.strip() != '':
782
                begin = self.text.split()[-1].lower()
783 784
            else:
                begin = ''
785 786
            hit_list = []       # list of matching nicks
            for user in user_list:
787 788
                if user.lower().startswith(begin):
                    hit_list.append(user)
789 790 791
            if len(hit_list) == 0:
                return
            self.hit_list = hit_list
792
            end = len(begin)
793
        else:
794
            begin = self.text[-len(after)-len(self.last_completion):-len(after)]
795
            self.hit_list.append(self.hit_list.pop(0)) # rotate list
796
            end = len(begin) + len(after)
797 798
        self.text = self.text[:-end]
        nick = self.hit_list[0] # take the first hit
799 800
        self.last_completion = nick
        self.text += nick +after
801
        self.key_end(False)
802

803
    def shell_completion(self, user_list, add_after):
804 805 806
        """
        Shell-like completion
        """
807
        if " " in self.text.strip() or not add_after:
808 809 810
            after = " " # don't put the "," if it's not the begining of the sentence
        else:
            after = config.get('after_completion', ',')+" "
811
        (y, x) = self._win.getyx()
812
        if self.text != '':
813
            begin = self.text.split()[-1].lower()
814 815
        else:
            begin = ''
816 817
        hit_list = []       # list of matching nicks
        for user in user_list:
818 819
            if user.lower().startswith(begin):
                hit_list.append(user)
820 821 822 823
        if len(hit_list) == 0:
            return
        end = False
        nick = ''
824 825
        last_completion = self.last_completion
        self.last_completion = True
826 827
        if len(hit_list) == 1:
            nick = hit_list[0] + after