windows.py 44 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 19 20 21
Define all the windows.
A window is a little part of the screen, for example the input window,
the text window, the roster window, etc.
A Tab (see tab.py) is composed of multiple Windows
22 23
"""

24 25
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
26
from os.path import isfile
27

28 29 30
import logging
log = logging.getLogger(__name__)

31 32 33
import locale
locale.setlocale(locale.LC_ALL, '')

34
import shlex
35
import curses
36
from config import config
37

38 39
from threading import Lock

40
from contact import Contact, Resource
41
from roster import RosterGroup, roster
42

43
from message import Line
44 45
from tab import MIN_WIDTH, MIN_HEIGHT

46 47
from sleekxmpp.xmlstream.stanzabase import JID

48
import theme
49

50 51
g_lock = Lock()

52
class Win(object):
53 54
    def __init__(self):
        pass
55

56
    def _resize(self, height, width, y, x, parent_win):
57
        self.height, self.width, self.x, self.y = height, width, x, y
58 59 60 61 62 63 64 65 66 67 68 69 70 71
        # 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()
72

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

    def addstr(self, *args):
        """
84
        Safe call to addstr
85
        """
86
        try:
87
            self._win.addstr(*args)
88 89
        except:
            pass
90

91 92 93 94
    def finish_line(self, color):
        """
        Write colored spaces until the end of line
        """
95
        (y, x) = self._win.getyx()
96 97 98
        size = self.width-x
        self.addnstr(' '*size, size, curses.color_pair(color))

99
class UserList(Win):
100 101
    def __init__(self):
        Win.__init__(self)
102 103 104
        self.color_role = {'moderator': theme.COLOR_USER_MODERATOR,
                           'participant':theme.COLOR_USER_PARTICIPANT,
                           'visitor':theme.COLOR_USER_VISITOR,
105 106
                           'none':theme.COLOR_USER_NONE,
                           '':theme.COLOR_USER_NONE
107
                           }
108
        self.color_show = {'xa':theme.COLOR_STATUS_XA,
109 110
                           'none':theme.COLOR_STATUS_NONE,
                           '':theme.COLOR_STATUS_NONE,
111 112 113
                           'dnd':theme.COLOR_STATUS_DND,
                           'away':theme.COLOR_STATUS_AWAY,
                           'chat':theme.COLOR_STATUS_CHAT
114
                           }
115 116

    def refresh(self, users):
117
        with g_lock:
118
            self._win.erase()
119 120 121 122 123 124 125 126 127 128 129
            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))
130
                self.addnstr(y, 1, user.nick, self.width-1, curses.color_pair(role_col))
131 132 133
                y += 1
                if y == self.height:
                    break
134
            self._refresh()
135

136 137
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
138 139 140
        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))
141

142
class Topic(Win):
143 144
    def __init__(self):
        Win.__init__(self)
145

146 147
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
148

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

160
class GlobalInfoBar(Win):
161 162
    def __init__(self):
        Win.__init__(self)
163

164 165
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
166

167
    def refresh(self, tabs, current):
168 169 170 171
        def compare_room(a):
            # return a.nb - b.nb
            return a.nb
        comp = lambda x: x.nb
172
        with g_lock:
173
            self._win.erase()
174
            self.addstr(0, 0, "[", curses.color_pair(theme.COLOR_INFORMATION_BAR))
175 176 177 178 179 180 181 182
            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
183
            (y, x) = self._win.getyx()
184
            self.addstr(y, x-1, '] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
185
            (y, x) = self._win.getyx()
186 187 188
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
                         curses.color_pair(theme.COLOR_INFORMATION_BAR))
189
            self._refresh()
190

191 192 193 194 195
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
196 197
    def __init__(self):
        Win.__init__(self)
198 199 200 201 202 203 204 205 206

    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
207
            self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
208 209 210 211 212 213

class PrivateInfoWin(InfoWin):
    """
    The live above the information window, displaying informations
    about the MUC user we are talking to
    """
214 215
    def __init__(self):
        InfoWin.__init__(self)
216

217 218
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
219 220

    def refresh(self, room):
221

222
        with g_lock:
223
            self._win.erase()
224 225 226
            self.write_room_name(room)
            self.print_scroll_position(room)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
227
            self._refresh()
228 229 230

    def write_room_name(self, room):
        (room_name, nick) = room.name.split('/', 1)
231
        self.addstr(nick, curses.color_pair(13))
232
        txt = ' from room %s' % room_name
233
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
234

235 236 237
class ConversationInfoWin(InfoWin):
    """
    The line above the information window, displaying informations
238
    about the user we are talking to
239
    """
240 241 242 243 244 245 246 247 248 249
    color_show = {'xa':theme.COLOR_STATUS_XA,
                  'none':theme.COLOR_STATUS_ONLINE,
                  '':theme.COLOR_STATUS_ONLINE,
                  'available':theme.COLOR_STATUS_ONLINE,
                  'dnd':theme.COLOR_STATUS_DND,
                  'away':theme.COLOR_STATUS_AWAY,
                  'chat':theme.COLOR_STATUS_CHAT,
                  'unavailable':theme.COLOR_STATUS_UNAVAILABLE
                  }

250 251
    def __init__(self):
        InfoWin.__init__(self)
252

253 254
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
255

256
    def refresh(self, jid, contact, text_buffer):
257 258 259
        # 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.
260 261 262 263 264 265 266 267 268 269 270 271 272
        jid = JID(jid)
        if contact:
            if jid.resource:
                resource = contact.get_resource_by_fulljid(jid.full)
            else:
                resource = contact.get_highest_priority_resource()
        else:
            resource = None
        # if contact is None, then resource is None too: user is not in the roster
        # so we don't know almost anything about it
        # If contact is a Contact, then
        # resource can now be a Resource: user is in the roster and online
        # or resource is None: user is in the roster but offline
273
        with g_lock:
274
            self._win.erase()
275 276 277 278 279
            self.write_contact_jid(jid)
            self.write_contact_informations(contact)
            self.write_resource_information(resource)
            self.print_scroll_position(text_buffer)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
280
            self._refresh()
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    def write_resource_information(self, resource):
        """
        Write the informations about the resource
        """
        if not resource:
            presence = "unavailable"
        else:
            presence = resource.get_presence()
        color = RosterWin.color_show[presence]
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
        self.addstr(" ", curses.color_pair(color))
        self.addstr(']', curses.color_pair(theme.COLOR_INFORMATION_BAR))

    def write_contact_informations(self, contact):
        """
        Write the informations about the contact
        """
299
        if not contact:
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
            self.addstr("(contact not in roster)", curses.color_pair(theme.COLOR_INFORMATION_BAR))
            return
        display_name = contact.get_name() or contact.get_bare_jid()
        self.addstr('%s '%(display_name), curses.color_pair(theme.COLOR_INFORMATION_BAR))

    def write_contact_jid(self, jid):
        """
        Just write the jid that we are talking to
        """
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
        self.addstr(jid.full, curses.color_pair(10))
        self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))

class ConversationStatusMessageWin(InfoWin):
    """
    The upper bar displaying the status message of the contact
    """
317 318
    def __init__(self):
        InfoWin.__init__(self)
319

320 321
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
322 323 324 325 326 327 328 329

    def refresh(self, jid, contact):
        jid = JID(jid)
        if contact:
            if jid.resource:
                resource = contact.get_resource_by_fulljid(jid.full)
            else:
                resource = contact.get_highest_priority_resource()
330
        else:
331 332 333 334 335 336 337 338 339 340
            resource = None
        with g_lock:
            self._win.erase()
            if resource:
                self.write_status_message(resource)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
            self._refresh()

    def write_status_message(self, resource):
        self.addstr(resource.get_status(), curses.color_pair(theme.COLOR_INFORMATION_BAR))
341

342 343 344 345 346
class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
347 348
    def __init__(self):
        InfoWin.__init__(self)
349

350 351
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
352 353

    def refresh(self, room):
354
        with g_lock:
355
            self._win.erase()
356 357 358 359 360 361
            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)
362
            self._refresh()
363 364 365 366

    def write_room_name(self, room):
        """
        """
367
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
368
        self.addnstr(room.name, len(room.name), curses.color_pair(13))
369
        self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
370 371 372 373 374 375

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

378 379 380 381 382 383 384 385 386
    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]+'…'
387
        self.addstr(nick, curses.color_pair(theme.COLOR_INFORMATION_BAR))
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

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

406
class TextWin(Win):
407
    """
408
    Just keep ONE single window for the text area and rewrite EVERYTHING
409
    on each change. (thanks weechat :o)
410
    """
411 412
    def __init__(self):
        Win.__init__(self)
413

414
    def build_lines_from_messages(self, messages):
415
        """
416 417
        From all the existing messages in the window, create the that will
        be displayed on the screen
418
        """
419 420
        lines = []
        for message in messages:
421 422 423
            if message == None:  # line separator
                lines.append(None)
                continue
424
            txt = message.txt
425 426
            if not txt:
                continue
427 428
            # length of the time
            offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1])
429
            if message.nickname and len(message.nickname) >= 30:
430
                nick = message.nickname[:30]+'…'
431 432 433 434
            else:
                nick = message.nickname
            if nick:
                offset += len(nick) + 2 # + nick + spaces length
435
            first = True
436
            this_line_was_broken_by_space = False
437 438 439 440
            while txt != '':
                if txt[:self.width-offset].find('\n') != -1:
                    limit = txt[:self.width-offset].find('\n')
                else:
441
                    # break between words if possible
442 443
                    if len(txt) >= self.width-offset:
                        limit = txt[:self.width-offset].rfind(' ')
444 445
                        this_line_was_broken_by_space = True
                        if limit <= 0:
446
                            limit = self.width-offset
447 448 449 450
                            this_line_was_broken_by_space = False
                    else:
                        limit = self.width-offset-1
                        this_line_was_broken_by_space = False
451
                color = message.user.color if message.user else None
452 453
                if not first:
                    nick = None
454 455 456
                    time = None
                else:
                    time = message.time
457
                l = Line(nick, color,
458
                         time,
459
                         txt[:limit], message.color,
460 461
                         offset,
                         message.colorized)
462
                lines.append(l)
463 464 465 466
                if this_line_was_broken_by_space:
                    txt = txt[limit+1:] # jump the space at the start of the line
                else:
                    txt = txt[limit:]
467 468
                if txt.startswith('\n'):
                    txt = txt[1:]
469
                first = False
470
        return lines
471

472
    def refresh(self, room):
473
        """
474 475
        Build the Line objects from the messages, and then write
        them in the text area
476
        """
477 478
        if self.height <= 0:
            return
479
        with g_lock:
480
            self._win.erase()
481 482 483 484 485 486 487 488 489 490 491
            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:
492
                self._win.move(y, 0)
493 494 495 496 497 498 499 500 501
                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)
502
                y += 1
503
            self._refresh()
504

505 506 507
    def write_line_separator(self):
        """
        """
508
        self._win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
509
        self.addnstr('- '*(self.width//2), self.width)
510
        self._win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
511

512
    def write_text(self, y, x, txt, color, colorized):
513
        """
514
        write the text of a line.
515
        """
516
        txt = txt
517 518
        if not colorized:
            if color:
519
                self._win.attron(curses.color_pair(color))
520
            self.addstr(y, x, txt)
521
            if color:
522
                self._win.attroff(curses.color_pair(color))
523 524 525 526 527 528 529

        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,
                }
530 531 532
            try:
                splitted = shlex.split(txt)
            except ValueError:
533 534 535 536 537
                # FIXME colors are disabled on too long words
                txt = txt.replace('"[', '').replace(']"', '')\
                    .replace('"{', '').replace('}"', '')\
                    .replace('"(', '').replace(')"', '')
                splitted = txt.split()
538
            for word in splitted:
539
                if word in list(special_words.keys()):
540
                    self.addstr(word, curses.color_pair(special_words[word]))
541
                elif word.startswith('(') and word.endswith(')'):
542 543 544
                    self.addstr('(', curses.color_pair(color))
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.addstr(')', curses.color_pair(color))
545
                elif word.startswith('{') and word.endswith('}'):
546
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
547
                elif word.startswith('[') and word.endswith(']'):
548
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
549
                else:
550
                    self.addstr(word, curses.color_pair(color))
551
                self._win.addch(' ')
552

553
    def write_nickname(self, nickname, color):
554 555 556 557
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
558
        if color:
559
            self._win.attron(curses.color_pair(color))
560
        self.addstr(nickname)
561
        if color:
562
            self._win.attroff(curses.color_pair(color))
563
        self.addstr("> ")
564

565
    def write_time(self, time):
566 567 568
        """
        Write the date on the yth line of the window
        """
569 570 571 572 573 574 575
        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))
576
        self.addstr(' ')
577

578 579
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
580

581 582
class HelpText(Win):
    """
583
    A Window just displaying a read-only message.
584 585 586
    Usually used to replace an Input when the tab is in
    command mode.
    """
587 588
    def __init__(self, text=''):
        Win.__init__(self)
589 590
        self.txt = text

591 592
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
593 594 595 596 597 598 599 600 601 602 603

    def refresh(self):
        with g_lock:
            self._win.erase()
            self.addstr(0, 0, self.txt[:self.width-1], curses.color_pair(theme.COLOR_INFORMATION_BAR))
            self.finish_line(theme.COLOR_INFORMATION_BAR)
            self._refresh()

    def do_command(self, key):
        return False

604 605
class Input(Win):
    """
606 607 608 609 610 611 612 613 614
    The simplest Input possible, provides just a way to edit a single line
    of text. It also has a clipboard, common to all Inputs.
    Doesn't have any history.
    It doesn't do anything when enter is pressed either.
    This should be herited for all kinds of Inputs, for example MessageInput
    or the little inputs in dataforms, etc, adding specific features (completion etc)
    It features two kinds of completion, but they have to be called from outside (the Tab),
    passing the list of items that can be used to complete. The completion can be used
    in a very flexible way.
615
    """
616 617
    clipboard = '' # A common clipboard for all the inputs, this makes
    # it easy cut and paste text between various input
618
    def __init__(self):
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
        self.key_func = {
            "KEY_LEFT": self.key_left,
            "M-D": self.key_left,
            "KEY_RIGHT": self.key_right,
            "M-C": self.key_right,
            "KEY_END": self.key_end,
            "KEY_HOME": self.key_home,
            "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,
            }

640
        Win.__init__(self)
641
        self.text = ''
642 643
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
644

645 646 647
    def is_empty(self):
        return len(self.text) == 0

648 649 650
    def resize(self, height, width, y, x, stdscr):

        self._resize(height, width, y, x, stdscr)
651
        self._win.erase()
652
        self.addnstr(0, 0, self.text, self.width-1)
653

654 655 656 657 658 659 660 661 662 663
    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
664
        for i in range(diff):
665
            self.key_left()
666
        return True
667 668 669 670 671 672 673 674 675 676 677

    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)
678
        for i in range(diff):
679
            self.key_right()
680
        return True
681 682 683 684 685 686 687 688 689 690 691

    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
692
        for i in range(diff):
693 694
            self.key_backspace(False)
        self.rewrite_text()
695
        return True
696 697 698 699 700 701 702

    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
703
        Input.clipboard = self.text[self.pos+self.line_pos:]
704 705
        self.text = self.text[:self.pos+self.line_pos]
        self.key_end()
706
        return True
707 708 709 710 711 712 713

    def delete_begining_of_line(self):
        """
        Cut the text from cursor to the begining of line
        """
        if self.pos+self.line_pos == 0:
            return
714
        Input.clipboard = self.text[:self.pos+self.line_pos]
715 716
        self.text = self.text[self.pos+self.line_pos:]
        self.key_home()
717
        return True
718 719 720 721 722

    def paste_clipboard(self):
        """
        Insert what is in the clipboard at the cursor position
        """
723
        if not Input.clipboard or len(Input.clipboard) == 0:
724
            return
725
        for letter in Input.clipboard:
726
            self.do_command(letter)
727
        return True
728

729
    def key_dc(self):
730 731 732
        """
        delete char just after the cursor
        """
733
        self.reset_completion()
734 735 736 737
        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()
738
        return True
739 740

    def key_home(self):
741 742 743
        """
        Go to the begining of line
        """
744
        self.reset_completion()
745
        self.pos = 0
746 747
        self.line_pos = 0
        self.rewrite_text()
748
        return True
749

750 751 752 753 754 755
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
756
        if len(self.text) >= self.width-1:
757 758
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
759
        else:
760 761 762
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
763
        return True
764 765

    def key_left(self):
766 767 768
        """
        Move the cursor one char to the left
        """
769
        self.reset_completion()
770
        (y, x) = self._win.getyx()
771 772 773
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
774
            self.pos -= 1
775
        self.rewrite_text()
776
        return True
777 778

    def key_right(self):
779 780 781
        """
        Move the cursor one char to the right
        """
782
        self.reset_completion()
783
        (y, x) = self._win.getyx()
784 785 786 787
        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):
788
            self.pos += 1
789
        self.rewrite_text()
790
        return True
791

792
    def key_backspace(self, reset=True):
793 794 795
        """
        Delete the char just before the cursor
        """
796
        self.reset_completion()
797
        (y, x) = self._win.getyx()
798 799 800 801
        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()
802 803
        if reset:
            self.rewrite_text()
804
        return True
805

806
    def auto_completion(self, word_list, add_after):
807
        """
808 809 810 811
        Complete the input, from a list of words
        if add_after is None, we use the value defined in completion
        plus a space, after the completion. If it's a string, we use it after the
        completion (with no additional space)
812
        """
813
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
814
            return # we don't complete if cursor is not at the end of line
815
        completion_type = config.get('completion', 'normal')
816
        if completion_type == 'shell' and self.text != '':
817
            self.shell_completion(word_list, add_after)
818
        else:
819
            self.normal_completion(word_list, add_after)
820
        return True
821 822

    def reset_completion(self):
823 824 825
        """
        Reset the completion list (called on ALL keys except tab)
        """
826
        self.hit_list = []
827
        self.last_completion = None
828

829
    def normal_completion(self, word_list, after):
830 831 832
        """
        Normal completion
        """
833
        (y, x) = self._win.getyx()
834
        if not self.last_completion:
835
            # begin is the begining of the nick we want to complete
836 837 838
            # if self.text.strip() != '' and\
            #         not self.text.endswith(after):
            if self.text.strip():
839
                begin = self.text.split()[-1].lower()
840 841
            else:
                begin = ''
842 843
            # else:
            #     begin = ''
844
            hit_list = []       # list of matching nicks
845 846 847
            for word in word_list:
                if word.lower().startswith(begin):
                    hit_list.append(word)
848 849 850
            if len(hit_list) == 0:
                return
            self.hit_list = hit_list