windows.py 46.8 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 53
class Win(object):
    def __init__(self, height, width, y, x, parent_win):
54
        self._resize(height, width, y, x, parent_win, True)
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
    def __init__(self, height, width, y, x, parent_win, visible):
103
        Win.__init__(self, height, width, y, x, parent_win)
104
        self.visible = visible
105 106 107
        self.color_role = {'moderator': theme.COLOR_USER_MODERATOR,
                           'participant':theme.COLOR_USER_PARTICIPANT,
                           'visitor':theme.COLOR_USER_VISITOR,
108 109
                           'none':theme.COLOR_USER_NONE,
                           '':theme.COLOR_USER_NONE
110
                           }
111
        self.color_show = {'xa':theme.COLOR_STATUS_XA,
112 113
                           'none':theme.COLOR_STATUS_NONE,
                           '':theme.COLOR_STATUS_NONE,
114 115 116
                           'dnd':theme.COLOR_STATUS_DND,
                           'away':theme.COLOR_STATUS_AWAY,
                           'chat':theme.COLOR_STATUS_CHAT
117
                           }
118 119

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

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

150
class Topic(Win):
151 152
    def __init__(self, height, width, y, x, parent_win, visible):
        self.visible = visible
153 154
        Win.__init__(self, height, width, y, x, parent_win)

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

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

171
class GlobalInfoBar(Win):
172 173 174 175 176
    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):
177
        self._resize(height, width, y, x, stdscr, visible)
178

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

205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
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
222
            self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
223 224 225 226 227 228 229 230 231 232

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):
233
        self._resize(height, width, y, x, stdscr, visible)
234 235 236 237

    def refresh(self, room):
        if not self.visible:
            return
238
        with g_lock:
239
            self._win.erase()
240 241 242
            self.write_room_name(room)
            self.print_scroll_position(room)
            self.finish_line(theme.COLOR_INFORMATION_BAR)
243
            self._refresh()
244 245 246

    def write_room_name(self, room):
        (room_name, nick) = room.name.split('/', 1)
247
        self.addstr(nick, curses.color_pair(13))
248
        txt = ' from room %s' % room_name
249
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
250

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

266 267 268 269 270 271
    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)

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

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    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
        """
317
        if not contact:
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
            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
    """
    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)

    def refresh(self, jid, contact):
        if not self.visible:
            return
        jid = JID(jid)
        if contact:
            if jid.resource:
                resource = contact.get_resource_by_fulljid(jid.full)
            else:
                resource = contact.get_highest_priority_resource()
350
        else:
351 352 353 354 355 356 357 358 359 360
            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))
361

362 363 364 365 366 367 368 369 370
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):
371
        self._resize(height, width, y, x, stdscr, visible)
372 373 374 375

    def refresh(self, room):
        if not self.visible:
            return
376
        with g_lock:
377
            self._win.erase()
378 379 380 381 382 383
            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)
384
            self._refresh()
385 386 387 388

    def write_room_name(self, room):
        """
        """
389
        self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR))
390
        self.addnstr(room.name, len(room.name), curses.color_pair(13))
391
        self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
392 393 394 395 396 397

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

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

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

428
class TextWin(Win):
429
    """
430
    Just keep ONE single window for the text area and rewrite EVERYTHING
431
    on each change. (thanks weechat :o)
432
    """
433
    def __init__(self, height, width, y, x, parent_win, visible):
434
        Win.__init__(self, height, width, y, x, parent_win)
435
        self.visible = visible
436

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

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

531 532 533
    def write_line_separator(self):
        """
        """
534
        self._win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
535
        self.addnstr('- '*(self.width//2), self.width)
536
        self._win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
537

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

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

579
    def write_nickname(self, nickname, color):
580 581 582 583
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
584
        if color:
585
            self._win.attron(curses.color_pair(color))
586
        self.addstr(nickname)
587
        if color:
588
            self._win.attroff(curses.color_pair(color))
589
        self.addstr("> ")
590

591
    def write_time(self, time):
592 593 594
        """
        Write the date on the yth line of the window
        """
595 596 597 598 599 600 601
        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))
602
        self.addstr(' ')
603

604
    def resize(self, height, width, y, x, stdscr, visible):
605
        self.visible = visible
606
        self._resize(height, width, y, x, stdscr, visible)
607

608 609
class HelpText(Win):
    """
610
    A Window just displaying a read-only message.
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
    Usually used to replace an Input when the tab is in
    command mode.
    """
    def __init__(self, height, width, y, x, parent_win, visible, text=''):
        self.visible = visible
        Win.__init__(self, height, width, y, x, parent_win)
        self.txt = text

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

    def refresh(self):
        if not self.visible:
            return
        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

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

670
        Win.__init__(self, height, width, y, x, stdscr)
671
        self.visible = visible
672
        self.text = ''
673 674
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
675

676 677 678
    def is_empty(self):
        return len(self.text) == 0

679 680 681 682
    def resize(self, height, width, y, x, stdscr, visible):
        self.visible = visible
        if not visible:
            return
683
        self._resize(height, width, y, x, stdscr, visible)
684
        self._win.erase()
685
        self.addnstr(0, 0, self.text, self.width-1)
686

687 688 689 690 691 692 693 694 695 696
    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
697
        for i in range(diff):
698
            self.key_left()
699
        return True
700 701 702 703 704 705 706 707 708 709 710

    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)
711
        for i in range(diff):
712
            self.key_right()
713
        return True
714 715 716 717 718 719 720 721 722 723 724

    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
725
        for i in range(diff):
726 727
            self.key_backspace(False)
        self.rewrite_text()
728
        return True
729 730 731 732 733 734 735

    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
736
        Input.clipboard = self.text[self.pos+self.line_pos:]
737 738
        self.text = self.text[:self.pos+self.line_pos]
        self.key_end()
739
        return True
740 741 742 743 744 745 746

    def delete_begining_of_line(self):
        """
        Cut the text from cursor to the begining of line
        """
        if self.pos+self.line_pos == 0:
            return
747
        Input.clipboard = self.text[:self.pos+self.line_pos]
748 749
        self.text = self.text[self.pos+self.line_pos:]
        self.key_home()
750
        return True
751 752 753 754 755

    def paste_clipboard(self):
        """
        Insert what is in the clipboard at the cursor position
        """
756
        if not Input.clipboard or len(Input.clipboard) == 0:
757
            return
758
        for letter in Input.clipboard:
759
            self.do_command(letter)
760
        return True
761

762
    def key_dc(self):
763 764 765
        """
        delete char just after the cursor
        """
766
        self.reset_completion()
767 768 769 770
        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()
771
        return True
772 773

    def key_home(self):
774 775 776
        """
        Go to the begining of line
        """
777
        self.reset_completion()
778
        self.pos = 0
779 780
        self.line_pos = 0
        self.rewrite_text()
781
        return True
782

783 784 785 786 787 788
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
789
        if len(self.text) >= self.width-1:
790 791
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
792
        else:
793 794 795
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
796
        return True
797 798

    def key_left(self):
799 800 801
        """
        Move the cursor one char to the left
        """
802
        self.reset_completion()
803
        (y, x) = self._win.getyx()
804 805 806
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
807
            self.pos -= 1
808
        self.rewrite_text()
809
        return True
810 811

    def key_right(self):
812 813 814
        """
        Move the cursor one char to the right
        """
815
        self.reset_completion()
816
        (y, x) = self._win.getyx()
817 818 819 820
        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):
821
            self.pos += 1
822
        self.rewrite_text()
823
        return True
824

825
    def key_backspace(self, reset=True):
826 827 828
        """
        Delete the char just before the cursor
        """
829
        self.reset_completion()
830
        (y, x) = self._win.getyx()
831 832 833 834
        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()
835 836
        if reset:
            self.rewrite_text()
837
        return True
838

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

    def reset_completion(self):
853 854 855
        """
        Reset the completion list (called on ALL keys except tab)
        """
856
        self.hit_list = []
857
        self.last_completion = None
858

859
    def normal_completion(self, user_list, add_after):
860 861 862
        """
        Normal completion
        """
863 864
        if add_after and (" " not in self.text.strip() or\
                self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "):
865
            after = config.get('after_completion', ',')+" "
866 867 868
            #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
869
        (y, x) = self._win.getyx()
870
        if not self.last_completion:
871
            # begin is the begining of the nick we w