windows.py 45.3 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 57 58
    def _resize(self, height, width, y, x, parent_win, visible):
        if not visible:
            return
59
        self.height, self.width, self.x, self.y = height, width, x, y
60 61 62 63 64 65 66 67 68 69 70 71 72 73
        # 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()
74

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

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

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

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

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

140 141 142 143
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
144
        self._resize(height, width, y, x, stdscr, visible)
145 146 147
        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))
148

149
class Topic(Win):
150 151
    def __init__(self):
        Win.__init__(self)
152

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

156
    def refresh(self, topic):
157 158
        # if not self.visible:
        #     return
159
        with g_lock:
160
            self._win.erase()
161
            self.addstr(0, 0, topic[:self.width-1], curses.color_pair(theme.COLOR_TOPIC_BAR))
162
            (y, x) = self._win.getyx()
163 164 165 166
            remaining_size = self.width - x
            if remaining_size:
                self.addnstr(' '*remaining_size, remaining_size,
                             curses.color_pair(theme.COLOR_INFORMATION_BAR))
167
            self._refresh()
168

169
class GlobalInfoBar(Win):
170 171
    def __init__(self):
        Win.__init__(self)
172 173

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

176
    def refresh(self, tabs, current):
177 178
        # if not self.visible:
        #     return
179 180 181 182
        def compare_room(a):
            # return a.nb - b.nb
            return a.nb
        comp = lambda x: x.nb
183
        with g_lock:
184
            self._win.erase()
185
            self.addstr(0, 0, "[", curses.color_pair(theme.COLOR_INFORMATION_BAR))
186 187 188 189 190 191 192 193
            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
194
            (y, x) = self._win.getyx()
195
            self.addstr(y, x-1, '] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
196
            (y, x) = self._win.getyx()
197 198 199
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
                         curses.color_pair(theme.COLOR_INFORMATION_BAR))
200
            self._refresh()
201

202 203 204 205 206
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
207 208
    def __init__(self):
        Win.__init__(self)
209 210 211 212 213 214 215 216 217

    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
218
            self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
219 220 221 222 223 224

class PrivateInfoWin(InfoWin):
    """
    The live above the information window, displaying informations
    about the MUC user we are talking to
    """
225 226
    def __init__(self):
        InfoWin.__init__(self)
227 228

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

    def refresh(self, room):
232 233
        # if not self.visible:
        #     return
234
        with g_lock:
235
            self._win.erase()
236 237 238
            self.write_room_name(room)
            self.print_scroll_position(room)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
239
            self._refresh()
240 241 242

    def write_room_name(self, room):
        (room_name, nick) = room.name.split('/', 1)
243
        self.addstr(nick, curses.color_pair(13))
244
        txt = ' from room %s' % room_name
245
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
246

247 248 249
class ConversationInfoWin(InfoWin):
    """
    The line above the information window, displaying informations
250
    about the user we are talking to
251
    """
252 253 254 255 256 257 258 259 260 261
    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
                  }

262 263
    def __init__(self):
        InfoWin.__init__(self)
264 265 266 267

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

268
    def refresh(self, jid, contact, text_buffer):
269 270
        # if not self.visible:
        #     return
271 272 273
        # 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.
274 275 276 277 278 279 280 281 282 283 284 285 286
        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
287
        with g_lock:
288
            self._win.erase()
289 290 291 292 293
            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)
294
            self._refresh()
295

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
    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
        """
313
        if not contact:
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
            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
    """
331 332
    def __init__(self):
        InfoWin.__init__(self)
333 334 335 336 337

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

    def refresh(self, jid, contact):
338 339
        # if not self.visible:
        #     return
340 341 342 343 344 345
        jid = JID(jid)
        if contact:
            if jid.resource:
                resource = contact.get_resource_by_fulljid(jid.full)
            else:
                resource = contact.get_highest_priority_resource()
346
        else:
347 348 349 350 351 352 353 354 355 356
            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))
357

358 359 360 361 362
class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
363 364
    def __init__(self):
        InfoWin.__init__(self)
365 366

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

    def refresh(self, room):
370 371
        # if not self.visible:
        #     return
372
        with g_lock:
373
            self._win.erase()
374 375 376 377 378 379
            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)
380
            self._refresh()
381 382 383 384

    def write_room_name(self, room):
        """
        """
385
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
386
        self.addnstr(room.name, len(room.name), curses.color_pair(13))
387
        self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
388 389 390 391 392 393

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

396 397 398 399 400 401 402 403 404
    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]+'…'
405
        self.addstr(nick, curses.color_pair(theme.COLOR_INFORMATION_BAR))
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421

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

424
class TextWin(Win):
425
    """
426
    Just keep ONE single window for the text area and rewrite EVERYTHING
427
    on each change. (thanks weechat :o)
428
    """
429 430
    def __init__(self):
        Win.__init__(self)
431

432
    def build_lines_from_messages(self, messages):
433
        """
434 435
        From all the existing messages in the window, create the that will
        be displayed on the screen
436
        """
437 438
        lines = []
        for message in messages:
439 440 441
            if message == None:  # line separator
                lines.append(None)
                continue
442
            txt = message.txt
443 444
            if not txt:
                continue
445 446
            # length of the time
            offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1])
447
            if message.nickname and len(message.nickname) >= 30:
448
                nick = message.nickname[:30]+'…'
449 450 451 452
            else:
                nick = message.nickname
            if nick:
                offset += len(nick) + 2 # + nick + spaces length
453
            first = True
454
            this_line_was_broken_by_space = False
455 456 457 458
            while txt != '':
                if txt[:self.width-offset].find('\n') != -1:
                    limit = txt[:self.width-offset].find('\n')
                else:
459
                    # break between words if possible
460 461
                    if len(txt) >= self.width-offset:
                        limit = txt[:self.width-offset].rfind(' ')
462 463
                        this_line_was_broken_by_space = True
                        if limit <= 0:
464
                            limit = self.width-offset
465 466 467 468
                            this_line_was_broken_by_space = False
                    else:
                        limit = self.width-offset-1
                        this_line_was_broken_by_space = False
469
                color = message.user.color if message.user else None
470 471
                if not first:
                    nick = None
472 473 474
                    time = None
                else:
                    time = message.time
475
                l = Line(nick, color,
476
                         time,
477
                         txt[:limit], message.color,
478 479
                         offset,
                         message.colorized)
480
                lines.append(l)
481 482 483 484
                if this_line_was_broken_by_space:
                    txt = txt[limit+1:] # jump the space at the start of the line
                else:
                    txt = txt[limit:]
485 486
                if txt.startswith('\n'):
                    txt = txt[1:]
487
                first = False
488
        return lines
489
        return lines[-len(messages):] # return only the needed number of lines
490

491
    def refresh(self, room):
492
        """
493 494
        Build the Line objects from the messages, and then write
        them in the text area
495
        """
496 497
        # if not self.visible:
        #     return
498 499
        if self.height <= 0:
            return
500
        with g_lock:
501
            self._win.erase()
502 503 504 505 506 507 508 509 510 511 512
            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:
513
                self._win.move(y, 0)
514 515 516 517 518 519 520 521 522
                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)
523
                y += 1
524
            self._refresh()
525

526 527 528
    def write_line_separator(self):
        """
        """
529
        self._win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
530
        self.addnstr('- '*(self.width//2), self.width)
531
        self._win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
532

533
    def write_text(self, y, x, txt, color, colorized):
534
        """
535
        write the text of a line.
536
        """
537
        txt = txt
538 539
        if not colorized:
            if color:
540
                self._win.attron(curses.color_pair(color))
541
            self.addstr(y, x, txt)
542
            if color:
543
                self._win.attroff(curses.color_pair(color))
544 545 546 547 548 549 550

        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,
                }
551 552 553
            try:
                splitted = shlex.split(txt)
            except ValueError:
554 555 556 557 558
                # FIXME colors are disabled on too long words
                txt = txt.replace('"[', '').replace(']"', '')\
                    .replace('"{', '').replace('}"', '')\
                    .replace('"(', '').replace(')"', '')
                splitted = txt.split()
559
            for word in splitted:
560
                if word in list(special_words.keys()):
561
                    self.addstr(word, curses.color_pair(special_words[word]))
562
                elif word.startswith('(') and word.endswith(')'):
563 564 565
                    self.addstr('(', curses.color_pair(color))
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.addstr(')', curses.color_pair(color))
566
                elif word.startswith('{') and word.endswith('}'):
567
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
568
                elif word.startswith('[') and word.endswith(']'):
569
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
570
                else:
571
                    self.addstr(word, curses.color_pair(color))
572
                self._win.addch(' ')
573

574
    def write_nickname(self, nickname, color):
575 576 577 578
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
579
        if color:
580
            self._win.attron(curses.color_pair(color))
581
        self.addstr(nickname)
582
        if color:
583
            self._win.attroff(curses.color_pair(color))
584
        self.addstr("> ")
585

586
    def write_time(self, time):
587 588 589
        """
        Write the date on the yth line of the window
        """
590 591 592 593 594 595 596
        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))
597
        self.addstr(' ')
598

599
    def resize(self, height, width, y, x, stdscr, visible):
600
        self.visible = visible
601
        self._resize(height, width, y, x, stdscr, visible)
602

603 604
class HelpText(Win):
    """
605
    A Window just displaying a read-only message.
606 607 608
    Usually used to replace an Input when the tab is in
    command mode.
    """
609 610
    def __init__(self, text=''):
        Win.__init__(self)
611 612 613 614 615 616
        self.txt = text

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

    def refresh(self):
617 618
        # if not self.visible:
        #     return
619 620 621 622 623 624 625 626 627
        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

628 629
class Input(Win):
    """
630 631 632 633 634 635 636 637 638
    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.
639
    """
640 641
    clipboard = '' # A common clipboard for all the inputs, this makes
    # it easy cut and paste text between various input
642
    def __init__(self):
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
        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,
            }

664
        Win.__init__(self)
665
        self.text = ''
666 667
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
668

669 670 671
    def is_empty(self):
        return len(self.text) == 0

672 673 674 675
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
676
        self._resize(height, width, y, x, stdscr, visible)
677
        self._win.erase()
678
        self.addnstr(0, 0, self.text, self.width-1)
679

680 681 682 683 684 685 686 687 688 689
    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
690
        for i in range(diff):
691
            self.key_left()
692
        return True
693 694 695 696 697 698 699 700 701 702 703

    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)
704
        for i in range(diff):
705
            self.key_right()
706
        return True
707 708 709 710 711 712 713 714 715 716 717

    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
718
        for i in range(diff):
719 720
            self.key_backspace(False)
        self.rewrite_text()
721
        return True
722 723 724 725 726 727 728

    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
729
        Input.clipboard = self.text[self.pos+self.line_pos:]
730 731
        self.text = self.text[:self.pos+self.line_pos]
        self.key_end()
732
        return True
733 734 735 736 737 738 739

    def delete_begining_of_line(self):
        """
        Cut the text from cursor to the begining of line
        """
        if self.pos+self.line_pos == 0:
            return
740
        Input.clipboard = self.text[:self.pos+self.line_pos]
741 742
        self.text = self.text[self.pos+self.line_pos:]
        self.key_home()
743
        return True
744 745 746 747 748

    def paste_clipboard(self):
        """
        Insert what is in the clipboard at the cursor position
        """
749
        if not Input.clipboard or len(Input.clipboard) == 0:
750
            return
751
        for letter in Input.clipboard:
752
            self.do_command(letter)
753
        return True
754

755
    def key_dc(self):
756 757 758
        """
        delete char just after the cursor
        """
759
        self.reset_completion()
760 761 762 763
        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()
764
        return True
765 766

    def key_home(self):
767 768 769
        """
        Go to the begining of line
        """
770
        self.reset_completion()
771
        self.pos = 0
772 773
        self.line_pos = 0
        self.rewrite_text()
774
        return True
775

776 777 778 779 780 781
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
782
        if len(self.text) >= self.width-1:
783 784
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
785
        else:
786 787 788
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
789
        return True
790 791

    def key_left(self):
792 793 794
        """
        Move the cursor one char to the left
        """
795
        self.reset_completion()
796
        (y, x) = self._win.getyx()
797 798 799
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
800
            self.pos -= 1
801
        self.rewrite_text()
802
        return True
803 804

    def key_right(self):
805 806 807
        """
        Move the cursor one char to the right
        """
808
        self.reset_completion()
809
        (y, x) = self._win.getyx()
810 811 812 813
        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):
814
            self.pos += 1
815
        self.rewrite_text()
816
        return True
817

818
    def key_backspace(self, reset=True):
819 820 821
        """
        Delete the char just before the cursor
        """
822
        self.reset_completion()
823
        (y, x) = self._win.getyx()
824 825 826 827
        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()
828 829
        if reset:
            self.rewrite_text()
830
        return True
831

832
    def auto_completion(self, user_list, add_after=True):
833 834 835
        """
        Complete the nickname
        """
836
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
837
            return # we don't complete if cursor is not at the end of line
838
        completion_type = config.get('completion', 'normal')
839
        if completion_type == 'shell' and self.text != '':
840
            self.shell_completion(user_list, add_after)
841
        else:
842
            self.normal_completion(user_list, add_after)
843
        return True
844 845

    def reset_completion(self):
846 847 848
        """
        Reset the completion list (called on ALL keys except tab)
        """
849
        self.hit_list = []
850
        self.last_completion = None
851

852
    def normal_completion(self, user_list, add_after):
853 854 855
        """
        Normal completion
        """
856 857
        if add_after and (" " not in self.text.strip() or\
                self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "):