windows.py 50.3 KB
Newer Older
1
# Copyright 2010-2011 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
from tabs import MIN_WIDTH, MIN_HEIGHT
45

46 47
from sleekxmpp.xmlstream.stanzabase import JID

48
import theme
49

50 51
g_lock = Lock()

52
LINES_NB_LIMIT = 4096
53

54
class Win(object):
55 56
    def __init__(self):
        pass
57

58
    def _resize(self, height, width, y, x, parent_win):
59
        self.height, self.width, self.x, self.y = height, width, x, y
60 61 62 63
        self._win = curses.newwin(height, width, y, x)

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

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

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

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

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

109 110 111 112 113 114 115 116 117 118 119
    def scroll_up(self):
        self.pos += 4

    def scroll_down(self):
        self.pos -= 4
        if self.pos < 0:
            self.pos = 0

    def draw_plus(self, y):
        self.addstr(y, self.width-2, '++', curses.color_pair(42))

120
    def refresh(self, users):
121
        with g_lock:
122
            self._win.erase()
123
            y = 0
124 125 126 127
            users = sorted(users)
            if self.pos >= len(users) and self.pos != 0:
                self.pos = len(users)-1
            for user in users[self.pos:]:
128 129 130 131 132 133 134 135 136
                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))
137
                self.addstr(y, 1, user.nick, curses.color_pair(role_col))
138 139 140
                y += 1
                if y == self.height:
                    break
141 142 143 144 145
            # draw indicators of position in the list
            if self.pos > 0:
                self.draw_plus(0)
            if self.pos + self.height < len(users):
                self.draw_plus(self.height-1)
146
            self._refresh()
147

148 149
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
150 151 152
        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))
153

154
class Topic(Win):
155 156
    def __init__(self):
        Win.__init__(self)
157
        self._message = ''
158

159 160
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
161

162
    def refresh(self, topic=None):
163
        with g_lock:
164
            self._win.erase()
165 166 167 168 169
            if topic:
                msg = topic[:self.width-1]
            else:
                msg = self._message[:self.width-1]
            self.addstr(0, 0, msg, curses.color_pair(theme.COLOR_TOPIC_BAR))
170
            (y, x) = self._win.getyx()
171 172 173 174
            remaining_size = self.width - x
            if remaining_size:
                self.addnstr(' '*remaining_size, remaining_size,
                             curses.color_pair(theme.COLOR_INFORMATION_BAR))
175
            self._refresh()
176

177 178 179
    def set_message(self, message):
        self._message = message

180
class GlobalInfoBar(Win):
181 182
    def __init__(self):
        Win.__init__(self)
183

184 185
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
186

187
    def refresh(self, tabs, current):
188 189 190 191
        def compare_room(a):
            # return a.nb - b.nb
            return a.nb
        comp = lambda x: x.nb
192
        with g_lock:
193
            self._win.erase()
194
            self.addstr(0, 0, "[", curses.color_pair(theme.COLOR_INFORMATION_BAR))
195 196 197
            sorted_tabs = sorted(tabs, key=comp)
            for tab in sorted_tabs:
                color = tab.get_color_state()
198 199 200
                if config.get('show_inactive_tabs', 'true') == 'false' and\
                        color == theme.COLOR_TAB_NORMAL:
                    continue
201 202 203 204 205
                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
206
            (y, x) = self._win.getyx()
207
            self.addstr(y, x-1, '] ', curses.color_pair(theme.COLOR_INFORMATION_BAR))
208
            (y, x) = self._win.getyx()
209 210 211
            remaining_size = self.width - x
            self.addnstr(' '*remaining_size, remaining_size,
                         curses.color_pair(theme.COLOR_INFORMATION_BAR))
212
            self._refresh()
213

214 215 216 217 218
class InfoWin(Win):
    """
    Base class for all the *InfoWin, used in various tabs. For example
    MucInfoWin, etc. Provides some useful methods.
    """
219 220
    def __init__(self):
        Win.__init__(self)
221

222
    def print_scroll_position(self, window):
223 224 225 226 227
        """
        Print, link in Weechat, a -PLUS(n)- where n
        is the number of available lines to scroll
        down
        """
228 229
        if window.pos > 0:
            plus = ' -PLUS(%s)-' % window.pos
230
            self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD)
231 232 233 234 235 236

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

240 241
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
242

243
    def refresh(self, room, window):
244

245
        with g_lock:
246
            self._win.erase()
247
            self.write_room_name(room)
248
            self.print_scroll_position(window)
249
            self.finish_line(theme.COLOR_INFORMATION_BAR)
250
            self._refresh()
251 252

    def write_room_name(self, room):
253 254
        jid = JID(room.name)
        room_name, nick = jid.bare, jid.resource
255
        self.addstr(nick, curses.color_pair(13))
256
        txt = ' from room %s' % room_name
257
        self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR))
258

259 260 261
class ConversationInfoWin(InfoWin):
    """
    The line above the information window, displaying informations
262
    about the user we are talking to
263
    """
264 265 266 267 268 269 270 271 272 273
    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
                  }

274 275
    def __init__(self):
        InfoWin.__init__(self)
276

277 278
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
279

280
    def refresh(self, jid, contact, text_buffer, window):
281 282 283
        # 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.
284 285 286 287 288 289 290 291 292 293 294 295 296
        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
297
        with g_lock:
298
            self._win.erase()
299 300 301
            self.write_contact_jid(jid)
            self.write_contact_informations(contact)
            self.write_resource_information(resource)
302
            self.print_scroll_position(window)
303
            self.finish_line(theme.COLOR_INFORMATION_BAR)
304
            self._refresh()
305

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
    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
        """
323
        if not contact:
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
            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
    """
341 342
    def __init__(self):
        InfoWin.__init__(self)
343

344 345
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
346 347 348 349 350 351 352 353

    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()
354
        else:
355 356 357 358 359 360 361 362 363 364
            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))
365

366 367 368 369 370
class MucInfoWin(InfoWin):
    """
    The line just above the information window, displaying informations
    about the MUC we are viewing
    """
371 372
    def __init__(self):
        InfoWin.__init__(self)
373

374 375
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
376

377
    def refresh(self, room, window=None):
378
        with g_lock:
379
            self._win.erase()
380 381 382 383
            self.write_room_name(room)
            self.write_own_nick(room)
            self.write_disconnected(room)
            self.write_role(room)
384 385
            if window:
                self.print_scroll_position(window)
386
            self.finish_line(theme.COLOR_INFORMATION_BAR)
387
            self._refresh()
388 389 390 391

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

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

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

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

431
class TextWin(Win):
432 433
    def __init__(self):
        Win.__init__(self)
434 435 436 437 438 439
        self.pos = 0
        self.built_lines = []   # Each new message is built and kept here.
        # on resize, we rebuild all the messages

    def scroll_up(self, dist=14):
        self.pos += dist
440 441 442 443
        if self.pos + self.height > len(self.built_lines):
            self.pos = len(self.built_lines) - self.height
            if self.pos < 0:
                self.pos = 0
444 445 446 447 448 449

    def scroll_down(self, dist=14):
        self.pos -= dist
        if self.pos <= 0:
            self.pos = 0

450 451 452 453 454 455 456 457 458 459 460 461 462 463
    def remove_line_separator(self):
        """
        Remove the line separator
        """
        if None in self.built_lines:
            self.built_lines.remove(None)

    def add_line_separator(self):
        """
        add a line separator at the end of messages list
        """
        if None not in self.built_lines:
            self.built_lines.append(None)

464 465 466 467 468 469 470 471 472 473 474 475
    def build_new_message(self, message):
        """
        Take one message, build it and add it to the list
        Return the number of lines that are built for the given
        message.
        """
        if message == None:  # line separator
            self.built_lines.append(None)
            return 0
        txt = message.txt
        if not txt:
            return 0
476
            # length of the time
477 478 479 480 481 482 483 484 485 486 487 488 489
        offset = 9+len(theme.CHAR_TIME_LEFT[:1])+len(theme.CHAR_TIME_RIGHT[:1])
        if message.nickname and len(message.nickname) >= 30:
            nick = message.nickname[:30]+'…'
        else:
            nick = message.nickname
        if nick:
            offset += len(nick) + 2 # + nick + spaces length
        first = True
        this_line_was_broken_by_space = False
        nb = 0
        while txt != '':
            if txt[:self.width-offset].find('\n') != -1:
                limit = txt[:self.width-offset].find('\n')
490
            else:
491 492 493 494 495 496
                # break between words if possible
                if len(txt) >= self.width-offset:
                    limit = txt[:self.width-offset].rfind(' ')
                    this_line_was_broken_by_space = True
                    if limit <= 0:
                        limit = self.width-offset
497 498
                        this_line_was_broken_by_space = False
                else:
499 500
                    limit = self.width-offset-1
                    this_line_was_broken_by_space = False
501
            color = message.user.color if message.user else message.nick_color
502 503 504
            if not first:
                nick = None
                time = None
505 506 507 508 509 510 511
            else:               # strftime is VERY slow, improve performance
                                # by calling it only one time here, and
                                # not at each refresh
                time = {'hour': '%s'%(message.time.strftime("%H"),),
                        'minute': '%s'%(message.time.strftime("%M"),),
                        'second': '%s'%(message.time.strftime("%S"),),
                        }
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
            l = Line(nick, color,
                     time,
                     txt[:limit], message.color,
                     offset,
                     message.colorized)
            self.built_lines.append(l)
            nb += 1
            if this_line_was_broken_by_space:
                txt = txt[limit+1:] # jump the space at the start of the line
            else:
                txt = txt[limit:]
            if txt.startswith('\n'):
                txt = txt[1:]
            first = False
        while len(self.built_lines) > LINES_NB_LIMIT:
            self.built_lines.pop(0)
        return nb
529

530
    def refresh(self, room):
531
        """
532 533
        Build the Line objects from the messages, and then write
        them in the text area
534
        """
535
        if self.height <= 0 or not self.built_lines:
536
            return
537 538 539 540 541
        if self.pos != 0:
            lines = self.built_lines[-self.height-self.pos:-self.pos]
        else:
            lines = self.built_lines[-self.height:]
        self._win.move(0, 0)
542
        with g_lock:
543
            self._win.erase()
544 545
            for y, line in enumerate(lines):
                if line is None:
546
                    self.write_line_separator()
547 548 549 550 551 552
                else:
                    if line.time:
                        self.write_time(line.time)
                    if line.nickname:
                        self.write_nickname(line.nickname, line.nickname_color)
                    self.write_text(y, line.text_offset, line.text, line.text_color, line.colorized)
553 554
                if y != self.height - 1:
                    self.addstr('\n')
555
            self._refresh()
556

557 558 559
    def write_line_separator(self):
        """
        """
560
        self.addnstr('- '*(self.width//2-1)+'-', self.width, curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR))
561

562
    def write_text(self, y, x, txt, color, colorized):
563
        """
564
        write the text of a line.
565
        """
566
        txt = txt
567 568
        if not colorized:
            if color:
569
                self._win.attron(curses.color_pair(color))
570
            self.addstr(y, x, txt)
571
            if color:
572
                self._win.attroff(curses.color_pair(color))
573 574 575 576 577 578 579

        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,
                }
580 581 582
            try:
                splitted = shlex.split(txt)
            except ValueError:
583 584 585 586 587
                # FIXME colors are disabled on too long words
                txt = txt.replace('"[', '').replace(']"', '')\
                    .replace('"{', '').replace('}"', '')\
                    .replace('"(', '').replace(')"', '')
                splitted = txt.split()
588
            for word in splitted:
589
                if word in list(special_words.keys()):
590
                    self.addstr(word, curses.color_pair(special_words[word]))
591
                elif word.startswith('(') and word.endswith(')'):
592 593 594
                    self.addstr('(', curses.color_pair(color))
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD))
                    self.addstr(')', curses.color_pair(color))
595
                elif word.startswith('{') and word.endswith('}'):
596
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD))
597
                elif word.startswith('[') and word.endswith(']'):
598
                    self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD))
599
                else:
600
                    self.addstr(word, curses.color_pair(color))
601
                self.addstr(' ')
602

603
    def write_nickname(self, nickname, color):
604 605 606 607
        """
        Write the nickname, using the user's color
        and return the number of written characters
        """
608
        if color:
609
            self._win.attron(curses.color_pair(color))
610
        self.addstr(nickname)
611
        if color:
612
            self._win.attroff(curses.color_pair(color))
613
        self.addstr("> ")
614

615
    def write_time(self, time):
616 617 618
        """
        Write the date on the yth line of the window
        """
619
        self.addstr(theme.CHAR_TIME_LEFT, curses.color_pair(theme.COLOR_TIME_LIMITER))
620
        self.addstr(time['hour'], curses.color_pair(theme.COLOR_TIME_NUMBERS))
621
        self.addstr(':', curses.color_pair(theme.COLOR_TIME_SEPARATOR))
622
        self.addstr(time['minute'], curses.color_pair(theme.COLOR_TIME_NUMBERS))
623
        self.addstr(':', curses.color_pair(theme.COLOR_TIME_SEPARATOR))
624 625
        self.addstr(time['second'], curses.color_pair(theme.COLOR_TIME_NUMBERS))
        self.addstr(theme.CHAR_TIME_RIGHT, curses.color_pair(theme.COLOR_TIME_LIMITER))
626
        self.addstr(' ')
627

628
    def resize(self, height, width, y, x, stdscr, room=None):
629
        self._resize(height, width, y, x, stdscr)
630 631 632 633 634 635 636
        if room:
            self.rebuild_everything(room)

    def rebuild_everything(self, room):
        self.built_lines = []
        for message in room.messages:
            self.build_new_message(message)
637

638 639
class HelpText(Win):
    """
640
    A Window just displaying a read-only message.
641 642 643
    Usually used to replace an Input when the tab is in
    command mode.
    """
644 645
    def __init__(self, text=''):
        Win.__init__(self)
646 647
        self.txt = text

648 649
    def resize(self, height, width, y, x, stdscr):
        self._resize(height, width, y, x, stdscr)
650 651 652 653 654 655 656 657 658 659 660

    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

661 662
class Input(Win):
    """
663 664 665 666 667 668 669 670 671
    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.
672
    """
673 674
    clipboard = '' # A common clipboard for all the inputs, this makes
    # it easy cut and paste text between various input
675
    def __init__(self):
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
        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,
            }

697
        Win.__init__(self)
698
        self.text = ''
699 700
        self.pos = 0            # cursor position
        self.line_pos = 0 # position (in self.text) of
701

702 703 704
    def is_empty(self):
        return len(self.text) == 0

705 706 707
    def resize(self, height, width, y, x, stdscr):

        self._resize(height, width, y, x, stdscr)
708
        self._win.erase()
709
        self.addnstr(0, 0, self.text, self.width-1)
710

711 712 713 714 715 716 717 718 719 720
    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
721
        for i in range(diff):
722
            self.key_left()
723
        return True
724 725 726 727 728 729 730 731 732 733 734

    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)
735
        for i in range(diff):
736
            self.key_right()
737
        return True
738 739 740 741 742 743 744 745 746 747 748

    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
749
        for i in range(diff):
750 751
            self.key_backspace(False)
        self.rewrite_text()
752
        return True
753 754 755 756 757 758 759

    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
760
        Input.clipboard = self.text[self.pos+self.line_pos:]
761 762
        self.text = self.text[:self.pos+self.line_pos]
        self.key_end()
763
        return True
764 765 766 767 768 769 770

    def delete_begining_of_line(self):
        """
        Cut the text from cursor to the begining of line
        """
        if self.pos+self.line_pos == 0:
            return
771
        Input.clipboard = self.text[:self.pos+self.line_pos]
772 773
        self.text = self.text[self.pos+self.line_pos:]
        self.key_home()
774
        return True
775 776 777 778 779

    def paste_clipboard(self):
        """
        Insert what is in the clipboard at the cursor position
        """
780
        if not Input.clipboard or len(Input.clipboard) == 0:
781
            return
782
        for letter in Input.clipboard:
783
            self.do_command(letter)
784
        return True
785

786
    def key_dc(self):
787 788 789
        """
        delete char just after the cursor
        """
790
        self.reset_completion()
791 792 793 794
        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()
795
        return True
796 797

    def key_home(self):
798 799 800
        """
        Go to the begining of line
        """
801
        self.reset_completion()
802
        self.pos = 0
803 804
        self.line_pos = 0
        self.rewrite_text()
805
        return True
806

807 808 809 810 811 812
    def key_end(self, reset=False):
        """
        Go to the end of line
        """
        if reset:
            self.reset_completion()
813
        if len(self.text) >= self.width-1:
814 815
            self.pos = self.width-1
            self.line_pos = len(self.text)-self.pos
816
        else:
817 818 819
            self.pos = len(self.text)
            self.line_pos = 0
        self.rewrite_text()
820
        return True
821 822

    def key_left(self):
823 824 825
        """
        Move the cursor one char to the left
        """
826
        self.reset_completion()
827
        (y, x) = self._win.getyx()
828 829 830
        if self.pos == self.width-1 and self.line_pos > 0:
            self.line_pos -= 1
        elif self.pos >= 1:
831
            self.pos -= 1
832
        self.rewrite_text()
833
        return True
834 835

    def key_right(self):
836 837 838
        """
        Move the cursor one char to the right
        """
839
        self.reset_completion()
840
        (y, x) = self._win.getyx()
841 842 843 844
        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):
845
            self.pos += 1
846
        self.rewrite_text()
847
        return True
848

849
    def key_backspace(self, reset=True):
850 851 852
        """
        Delete the char just before the cursor
        """
853
        self.reset_completion()
854
        (y, x) = self._win.getyx()
855 856 857 858
        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()
859 860
        if reset:
            self.rewrite_text()
861
        return True
862

863
    def auto_completion(self, word_list, add_after):
864
        """
865 866 867 868
        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)
869
        """
870
        if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
871
            return # we don't complete if cursor is not at the end of line
872
        completion_type = config.get('completion', 'normal')
873
        if completion_type == 'shell' and self.text != '':
874
            self.shell_completion(word_list, add_after)
875
        else:
876
            self.normal_completion(word_list, add_after)