diff options
Diffstat (limited to 'src/windows.py')
-rw-r--r-- | src/windows.py | 1328 |
1 files changed, 1328 insertions, 0 deletions
diff --git a/src/windows.py b/src/windows.py new file mode 100644 index 00000000..dee1aa9d --- /dev/null +++ b/src/windows.py @@ -0,0 +1,1328 @@ +# Copyright 2010 Le Coz Florent <louiz@louiz.org> +# +# 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/>. + +""" +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 +""" + +from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset, + gettext as _) +from os.path import isfile + +import logging +log = logging.getLogger(__name__) + +import locale +locale.setlocale(locale.LC_ALL, '') + +import shlex +import curses +from config import config + +from threading import Lock + +from contact import Contact, Resource +from roster import RosterGroup, roster + +from message import Line +from tab import MIN_WIDTH, MIN_HEIGHT + +from sleekxmpp.xmlstream.stanzabase import JID + +import theme + +g_lock = Lock() + +class Win(object): + def __init__(self, height, width, y, x, parent_win): + self._resize(height, width, y, x, parent_win, True) + + def _resize(self, height, width, y, x, parent_win, visible): + if not visible: + return + self.height, self.width, self.x, self.y = height, width, x, y + # 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() + + def addnstr(self, *args): + """ + Safe call to addnstr + """ + try: + self._win.addnstr(*args) + except: + pass + + def addstr(self, *args): + """ + Safe call to addstr + """ + try: + self._win.addstr(*args) + except: + pass + + def finish_line(self, color): + """ + Write colored spaces until the end of line + """ + (y, x) = self._win.getyx() + size = self.width-x + self.addnstr(' '*size, size, curses.color_pair(color)) + +class UserList(Win): + def __init__(self, height, width, y, x, parent_win, visible): + Win.__init__(self, height, width, y, x, parent_win) + self.visible = visible + self.color_role = {'moderator': theme.COLOR_USER_MODERATOR, + 'participant':theme.COLOR_USER_PARTICIPANT, + 'visitor':theme.COLOR_USER_VISITOR, + 'none':theme.COLOR_USER_NONE, + '':theme.COLOR_USER_NONE + } + self.color_show = {'xa':theme.COLOR_STATUS_XA, + 'none':theme.COLOR_STATUS_NONE, + '':theme.COLOR_STATUS_NONE, + 'dnd':theme.COLOR_STATUS_DND, + 'away':theme.COLOR_STATUS_AWAY, + 'chat':theme.COLOR_STATUS_CHAT + } + + def refresh(self, users): + if not self.visible: + return + with g_lock: + self._win.erase() + 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)) + self.addnstr(y, 1, user.nick, self.width-1, curses.color_pair(role_col)) + y += 1 + if y == self.height: + break + self._refresh() + + def resize(self, height, width, y, x, stdscr, visible): + self.visible = visible + if not visible: + return + self._resize(height, width, y, x, stdscr, visible) + 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)) + +class Topic(Win): + 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): + self._resize(height, width, y, x, stdscr, visible) + + def refresh(self, topic): + if not self.visible: + return + with g_lock: + self._win.erase() + self.addstr(0, 0, topic[:self.width-1], curses.color_pair(theme.COLOR_TOPIC_BAR)) + (y, x) = self._win.getyx() + remaining_size = self.width - x + if remaining_size: + self.addnstr(' '*remaining_size, remaining_size, + curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self._refresh() + +class GlobalInfoBar(Win): + 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): + self._resize(height, width, y, x, stdscr, visible) + + def refresh(self, tabs, current): + if not self.visible: + return + def compare_room(a): + # return a.nb - b.nb + return a.nb + comp = lambda x: x.nb + with g_lock: + self._win.erase() + self.addstr(0, 0, "[", curses.color_pair(theme.COLOR_INFORMATION_BAR)) + 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 + (y, x) = self._win.getyx() + self.addstr(y, x-1, '] ', curses.color_pair(theme.COLOR_INFORMATION_BAR)) + (y, x) = self._win.getyx() + remaining_size = self.width - x + self.addnstr(' '*remaining_size, remaining_size, + curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self._refresh() + +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 + self.addstr(plus, curses.color_pair(theme.COLOR_SCROLLABLE_NUMBER) | curses.A_BOLD) + +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): + self._resize(height, width, y, x, stdscr, visible) + + def refresh(self, room): + if not self.visible: + return + with g_lock: + self._win.erase() + self.write_room_name(room) + self.print_scroll_position(room) + self.finish_line(theme.COLOR_INFORMATION_BAR) + self._refresh() + + def write_room_name(self, room): + (room_name, nick) = room.name.split('/', 1) + self.addstr(nick, curses.color_pair(13)) + txt = ' from room %s' % room_name + self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + +class ConversationInfoWin(InfoWin): + """ + The line above the information window, displaying informations + about the user we are talking to + """ + 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 + } + + 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, text_buffer): + if not self.visible: + return + # 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. + 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 + with g_lock: + self._win.erase() + 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) + self._refresh() + + 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 + """ + if not contact: + 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() + else: + 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)) + +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): + self._resize(height, width, y, x, stdscr, visible) + + def refresh(self, room): + if not self.visible: + return + with g_lock: + self._win.erase() + 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) + self._refresh() + + def write_room_name(self, room): + """ + """ + self.addstr('[', curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self.addnstr(room.name, len(room.name), curses.color_pair(13)) + self.addstr('] ', curses.color_pair(theme.COLOR_INFORMATION_BAR)) + + def write_disconnected(self, room): + """ + Shows a message if the room is not joined + """ + if not room.joined: + self.addstr(' -!- Not connected ', curses.color_pair(theme.COLOR_INFORMATION_BAR)) + + 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]+'…' + self.addstr(nick, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + + 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+')' + self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + +class TextWin(Win): + """ + Just keep ONE single window for the text area and rewrite EVERYTHING + on each change. (thanks weechat :o) + """ + def __init__(self, height, width, y, x, parent_win, visible): + Win.__init__(self, height, width, y, x, parent_win) + self.visible = visible + + def build_lines_from_messages(self, messages): + """ + From all the existing messages in the window, create the that will + be displayed on the screen + """ + lines = [] + for message in messages: + if message == None: # line separator + lines.append(None) + continue + txt = message.txt + if not txt: + continue + # length of the time + 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 + while txt != '': + if txt[:self.width-offset].find('\n') != -1: + limit = txt[:self.width-offset].find('\n') + else: + # 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 + this_line_was_broken_by_space = False + else: + limit = self.width-offset-1 + this_line_was_broken_by_space = False + color = message.user.color if message.user else None + if not first: + nick = None + time = None + else: + time = message.time + l = Line(nick, color, + time, + txt[:limit], message.color, + offset, + message.colorized) + lines.append(l) + 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 + return lines + return lines[-len(messages):] # return only the needed number of lines + + def refresh(self, room): + """ + Build the Line objects from the messages, and then write + them in the text area + """ + if not self.visible: + return + if self.height <= 0: + return + with g_lock: + self._win.erase() + 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: + self._win.move(y, 0) + 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) + y += 1 + self._refresh() + + def write_line_separator(self): + """ + """ + self._win.attron(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR)) + self.addnstr('- '*(self.width//2), self.width) + self._win.attroff(curses.color_pair(theme.COLOR_NEW_TEXT_SEPARATOR)) + + def write_text(self, y, x, txt, color, colorized): + """ + write the text of a line. + """ + txt = txt + if not colorized: + if color: + self._win.attron(curses.color_pair(color)) + self.addstr(y, x, txt) + if color: + self._win.attroff(curses.color_pair(color)) + + 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, + } + try: + splitted = shlex.split(txt) + except ValueError: + # FIXME colors are disabled on too long words + txt = txt.replace('"[', '').replace(']"', '')\ + .replace('"{', '').replace('}"', '')\ + .replace('"(', '').replace(')"', '') + splitted = txt.split() + for word in splitted: + if word in list(special_words.keys()): + self.addstr(word, curses.color_pair(special_words[word])) + elif word.startswith('(') and word.endswith(')'): + self.addstr('(', curses.color_pair(color)) + self.addstr(word[1:-1], curses.color_pair(theme.COLOR_CURLYBRACKETED_WORD)) + self.addstr(')', curses.color_pair(color)) + elif word.startswith('{') and word.endswith('}'): + self.addstr(word[1:-1], curses.color_pair(theme.COLOR_ACCOLADE_WORD)) + elif word.startswith('[') and word.endswith(']'): + self.addstr(word[1:-1], curses.color_pair(theme.COLOR_BRACKETED_WORD)) + else: + self.addstr(word, curses.color_pair(color)) + self._win.addch(' ') + + def write_nickname(self, nickname, color): + """ + Write the nickname, using the user's color + and return the number of written characters + """ + if color: + self._win.attron(curses.color_pair(color)) + self.addstr(nickname) + if color: + self._win.attroff(curses.color_pair(color)) + self.addstr("> ") + + def write_time(self, time): + """ + Write the date on the yth line of the window + """ + 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)) + self.addstr(' ') + + def resize(self, height, width, y, x, stdscr, visible): + self.visible = visible + self._resize(height, width, y, x, stdscr, visible) + +class HelpText(Win): + """ + A Window just displaying a read-only message. + 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 + +class Input(Win): + """ + 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. + """ + 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): + 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, + } + + Win.__init__(self, height, width, y, x, stdscr) + self.visible = visible + self.text = '' + self.pos = 0 # cursor position + self.line_pos = 0 # position (in self.text) of + + def is_empty(self): + return len(self.text) == 0 + + def resize(self, height, width, y, x, stdscr, visible): + self.visible = visible + if not visible: + return + self._resize(height, width, y, x, stdscr, visible) + self._win.erase() + self.addnstr(0, 0, self.text, self.width-1) + + 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 + for i in range(diff): + self.key_left() + return True + + 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) + for i in range(diff): + self.key_right() + return True + + 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 + for i in range(diff): + self.key_backspace(False) + self.rewrite_text() + return True + + 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 + Input.clipboard = self.text[self.pos+self.line_pos:] + self.text = self.text[:self.pos+self.line_pos] + self.key_end() + return True + + def delete_begining_of_line(self): + """ + Cut the text from cursor to the begining of line + """ + if self.pos+self.line_pos == 0: + return + Input.clipboard = self.text[:self.pos+self.line_pos] + self.text = self.text[self.pos+self.line_pos:] + self.key_home() + return True + + def paste_clipboard(self): + """ + Insert what is in the clipboard at the cursor position + """ + if not Input.clipboard or len(Input.clipboard) == 0: + return + for letter in Input.clipboard: + self.do_command(letter) + return True + + def key_dc(self): + """ + delete char just after the cursor + """ + self.reset_completion() + 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() + return True + + def key_home(self): + """ + Go to the begining of line + """ + self.reset_completion() + self.pos = 0 + self.line_pos = 0 + self.rewrite_text() + return True + + def key_end(self, reset=False): + """ + Go to the end of line + """ + if reset: + self.reset_completion() + if len(self.text) >= self.width-1: + self.pos = self.width-1 + self.line_pos = len(self.text)-self.pos + else: + self.pos = len(self.text) + self.line_pos = 0 + self.rewrite_text() + return True + + def key_left(self): + """ + Move the cursor one char to the left + """ + self.reset_completion() + (y, x) = self._win.getyx() + if self.pos == self.width-1 and self.line_pos > 0: + self.line_pos -= 1 + elif self.pos >= 1: + self.pos -= 1 + self.rewrite_text() + return True + + def key_right(self): + """ + Move the cursor one char to the right + """ + self.reset_completion() + (y, x) = self._win.getyx() + 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): + self.pos += 1 + self.rewrite_text() + return True + + def key_backspace(self, reset=True): + """ + Delete the char just before the cursor + """ + self.reset_completion() + (y, x) = self._win.getyx() + 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() + if reset: + self.rewrite_text() + return True + + def auto_completion(self, user_list, add_after=True): + """ + Complete the nickname + """ + if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0 + return # we don't complete if cursor is not at the end of line + completion_type = config.get('completion', 'normal') + if completion_type == 'shell' and self.text != '': + self.shell_completion(user_list, add_after) + else: + self.normal_completion(user_list, add_after) + return True + + def reset_completion(self): + """ + Reset the completion list (called on ALL keys except tab) + """ + self.hit_list = [] + self.last_completion = None + + def normal_completion(self, user_list, add_after): + """ + Normal completion + """ + if add_after and (" " not in self.text.strip() or\ + self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "): + after = config.get('after_completion', ',')+" " + #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 + (y, x) = self._win.getyx() + if not self.last_completion: + # begin is the begining of the nick we want to complete + if self.text.strip() != '': + begin = self.text.split()[-1].lower() + else: + begin = '' + hit_list = [] # list of matching nicks + for user in user_list: + if user.lower().startswith(begin): + hit_list.append(user) + if len(hit_list) == 0: + return + self.hit_list = hit_list + end = len(begin) + else: + begin = self.text[-len(after)-len(self.last_completion):-len(after)] + self.hit_list.append(self.hit_list.pop(0)) # rotate list + end = len(begin) + len(after) + self.text = self.text[:-end] + nick = self.hit_list[0] # take the first hit + self.last_completion = nick + self.text += nick +after + self.key_end(False) + + def shell_completion(self, user_list, add_after): + """ + Shell-like completion + """ + if " " in self.text.strip() or not add_after: + after = " " # don't put the "," if it's not the begining of the sentence + else: + after = config.get('after_completion', ',')+" " + (y, x) = self._win.getyx() + if self.text != '': + begin = self.text.split()[-1].lower() + else: + begin = '' + hit_list = [] # list of matching nicks + for user in user_list: + if user.lower().startswith(begin): + hit_list.append(user) + if len(hit_list) == 0: + return + end = False + nick = '' + last_completion = self.last_completion + self.last_completion = True + if len(hit_list) == 1: + nick = hit_list[0] + after + self.last_completion = False + elif last_completion: + for n in hit_list: + if begin.lower() == n.lower(): + nick = n+after # user DO want this completion (tabbed twice on it) + self.last_completion = False + if nick == '': + while not end and len(nick) < len(hit_list[0]): + nick = hit_list[0][:len(nick)+1] + for hit in hit_list: + if not hit.lower().startswith(nick.lower()): + end = True + break + if end: + nick = nick[:-1] + x -= len(begin) + self.text = self.text[:-len(begin)] + self.text += nick + self.key_end(False) + + def do_command(self, key, reset=True): + if key in self.key_func: + return self.key_func[key]() + if not key or len(key) > 1: + return False # ignore non-handled keyboard shortcuts + self.reset_completion() + self.text = self.text[:self.pos+self.line_pos]+key+self.text[self.pos+self.line_pos:] + (y, x) = self._win.getyx() + if x == self.width-1: + self.line_pos += 1 + else: + self.pos += 1 + if reset: + self.rewrite_text() + return True + + def get_text(self): + """ + Clear the input and return the text entered so far + """ + return self.text + + def rewrite_text(self): + """ + Refresh the line onscreen, from the pos and pos_line + """ + with g_lock: + self._win.erase() + self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) + cursor_pos = self.pos + self.addstr(0, cursor_pos, '') # WTF, this works but .move() doesn't… + self._refresh() + + def refresh(self): + if not self.visible: + return + self.rewrite_text() + + def clear_text(self): + self.text = '' + self.pos = 0 + self.line_pos = 0 + self.rewrite_text() + +class MessageInput(Input): + """ + The input featuring history and that is being used in + Conversation, Muc and Private tabs + """ + history = list() # The history is common to all MessageInput + + def __init__(self, height, width, y, x, stdscr, visible): + Input.__init__(self, height, width, y, x, stdscr, visible) + self.last_completion = None + self.histo_pos = 0 + self.key_func["KEY_UP"] = self.key_up + self.key_func["M-A"] = self.key_up + self.key_func["KEY_DOWN"] = self.key_down + self.key_func["M-B"] = self.key_down + + def key_up(self): + """ + Get the previous line in the history + """ + if not len(MessageInput.history): + return + self.reset_completion() + self._win.erase() + if self.histo_pos >= 0: + self.histo_pos -= 1 + self.text = MessageInput.history[self.histo_pos+1] + self.key_end() + + def key_down(self): + """ + Get the next line in the history + """ + if not len(MessageInput.history): + return + self.reset_completion() + if self.histo_pos < len(MessageInput.history)-1: + self.histo_pos += 1 + self.text = self.history[self.histo_pos] + self.key_end() + else: + self.histo_pos = len(MessageInput.history)-1 + self.text = '' + self.pos = 0 + self.line_pos = 0 + self.rewrite_text() + + def key_enter(self): + txt = self.get_text() + if len(txt) != 0: + self.history.append(txt) + self.histo_pos = len(self.history)-1 + self.clear_text() + return txt + +class CommandInput(Input): + """ + An input with an help message in the left, with three given callbacks: + one when when successfully 'execute' the command and when we abort it. + The last callback is optional and is called on any input key + This input is used, for example, in the RosterTab when, to replace the + HelpMessage when a command is started + The on_input callback + """ + def __init__(self, height, width, y, x, stdscr, visible, + help_message, on_abort, on_success, on_input=None): + Input.__init__(self, height, width, y, x, stdscr, visible) + self.on_abort = on_abort + self.on_success = on_success + self.on_input = on_input + self.help_message = help_message + self.key_func['^J'] = self.success + self.key_func['^M'] = self.success + self.key_func['\n'] = self.success + self.key_func['^G'] = self.abort + + def do_command(self, key): + res = Input.do_command(self, key) + if self.on_input: + self.on_input(self.get_text()) + return res + + def success(self): + """ + call the success callback, passing the text as argument + """ + self.on_input = None + res = self.on_success(self.get_text()) + return res + + def abort(self): + """ + Call the abort callback, passing the text as argument + """ + self.on_input = None + return self.on_abort(self.get_text()) + + def rewrite_text(self): + """ + Rewrite the text just like a normal input, but with the instruction + on the left + """ + with g_lock: + self._win.erase() + self.addstr(self.help_message, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + cursor_pos = self.pos + len(self.help_message) + if len(self.help_message): + self.addstr(' ') + cursor_pos += 1 + self.addstr(self.text[self.line_pos:self.line_pos+self.width-1]) + self.addstr(0, cursor_pos, '') # WTF, this works but .move() doesn't… + self._refresh() + +class VerticalSeparator(Win): + """ + Just a one-column window, with just a line in it, that is + refreshed only on resize, but never on refresh, for efficiency + """ + def __init__(self, height, width, y, x, parent_win, visible): + Win.__init__(self, height, width, y, x, parent_win) + self.visible = visible + + def rewrite_line(self): + with g_lock: + self._win.vline(0, 0, curses.ACS_VLINE, self.height, curses.color_pair(theme.COLOR_VERTICAL_SEPARATOR)) + self._refresh() + + def resize(self, height, width, y, x, stdscr, visible): + self.visible = visible + self._resize(height, width, y, x, stdscr, visible) + if not visible: + return + + def refresh(self): + if not self.visible: + return + self.rewrite_line() + +class RosterWin(Win): + 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 + } + + def __init__(self, height, width, y, x, parent_win, visible): + self.visible = visible + Win.__init__(self, height, width, y, x, parent_win) + self.pos = 0 # cursor position in the contact list + self.start_pos = 1 # position of the start of the display + self.roster_len = 0 + self.selected_row = None + + def resize(self, height, width, y, x, stdscr, visible): + self._resize(height, width, y, x, stdscr, visible) + self.visible = visible + + def move_cursor_down(self): + if self.pos < self.roster_len-1: + self.pos += 1 + if self.pos == self.start_pos-1 + self.height-1: + self.scroll_down() + + def move_cursor_up(self): + if self.pos > 0: + self.pos -= 1 + if self.pos == self.start_pos-2: + self.scroll_up() + + def scroll_down(self): + self.start_pos += 8 + + def scroll_up(self): + self.start_pos -= 8 + + def refresh(self, roster): + """ + We get the roster object + """ + if not self.visible: + return + with g_lock: + self.roster_len = len(roster) + while self.roster_len and self.pos >= self.roster_len: + self.move_cursor_up() + self._win.erase() + self.draw_roster_information(roster) + y = 1 + for group in roster.get_groups(): + if group.get_nb_connected_contacts() == 0: + continue # Ignore empty groups + # This loop is really REALLY ugly :^) + if y-1 == self.pos: + self.selected_row = group + if y >= self.start_pos: + self.draw_group(y-self.start_pos+1, group, y-1==self.pos) + y += 1 + if group.folded: + continue + for contact in group.get_contacts(roster._contact_filter): + if config.get('roster_show_offline', 'false') == 'false' and\ + contact.get_nb_resources() == 0: + continue + if y-1 == self.pos: + self.selected_row = contact + if y-self.start_pos+1 == self.height: + break + if y >= self.start_pos: + self.draw_contact_line(y-self.start_pos+1, contact, y-1==self.pos) + y += 1 + if not contact._folded: + for resource in contact.get_resources(): + if y-1 == self.pos: + self.selected_row = resource + if y-self.start_pos+1 == self.height: + break + if y >= self.start_pos: + self.draw_resource_line(y-self.start_pos+1, resource, y-1==self.pos) + y += 1 + if y-self.start_pos+1 == self.height: + break + if self.start_pos > 1: + self.draw_plus(1) + if self.start_pos + self.height-2 < self.roster_len: + self.draw_plus(self.height-1) + self._refresh() + + def draw_plus(self, y): + """ + Draw the indicator that shows that + the list is longer than what is displayed + """ + self.addstr(y, self.width-5, '++++', curses.color_pair(42)) + + def draw_roster_information(self, roster): + """ + """ + self.addstr('%s contacts' % roster.get_contact_len(), curses.color_pair(12)) + self.finish_line(12) + + def draw_group(self, y, group, colored): + """ + Draw a groupname on a line + """ + if colored: + self._win.attron(curses.color_pair(14)) + if group.folded: + self.addstr(y, 0, '[+] ') + else: + self.addstr(y, 0, '[-] ') + self.addstr(y, 4, group.name) + if colored: + self._win.attroff(curses.color_pair(14)) + + def draw_contact_line(self, y, contact, colored): + """ + Draw on a line all informations about one contact. + This is basically the highest priority resource's informations + Use 'color' to draw the jid/display_name to show what is + the currently selected contact in the list + """ + resource = contact.get_highest_priority_resource() + if not resource: + # There's no online resource + presence = 'unavailable' + folder = ' ' + nb = '' + else: + presence = resource.get_presence() + folder = '[+]' if contact._folded else '[-]' + nb = '(%s)' % (contact.get_nb_resources(),) + color = RosterWin.color_show[presence] + if contact.get_name(): + display_name = '%s (%s) %s' % (contact.get_name(), + contact.get_bare_jid(), nb,) + else: + display_name = '%s %s' % (contact.get_bare_jid(), nb,) + self.addstr(y, 1, " ", curses.color_pair(color)) + if resource: + self.addstr(y, 2, ' [+]' if contact._folded else ' [-]') + self.addstr(' ') + if colored: + self.addstr(display_name, curses.color_pair(14)) + else: + self.addstr(display_name) + + def draw_resource_line(self, y, resource, colored): + """ + Draw a specific resource line + """ + color = RosterWin.color_show[resource.get_presence()] + self.addstr(y, 4, " ", curses.color_pair(color)) + if colored: + self.addstr(y, 6, resource.get_jid().full, curses.color_pair(14)) + else: + self.addstr(y, 6, resource.get_jid().full) + + def get_selected_row(self): + return self.selected_row + +class ContactInfoWin(Win): + 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): + self._resize(height, width, y, x, stdscr, visible) + self.visible = visible + + def draw_contact_info(self, resource, jid=None): + """ + draw the contact information + """ + jid = jid or resource.get_jid().full + if resource: + presence = resource.get_presence() + else: + presence = 'unavailable' + self.addstr(0, 0, jid, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self.addstr(' (%s)'%(presence,), curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self.finish_line(theme.COLOR_INFORMATION_BAR) + + def draw_group_info(self, group): + """ + draw the group information + """ + self.addstr(0, 0, group.name, curses.color_pair(theme.COLOR_INFORMATION_BAR)) + self.finish_line(theme.COLOR_INFORMATION_BAR) + + def refresh(self, selected_row): + if not self.visible: + return + with g_lock: + self._win.erase() + if isinstance(selected_row, RosterGroup): + self.draw_group_info(selected_row) + elif isinstance(selected_row, Contact): + self.draw_contact_info(selected_row.get_highest_priority_resource(), + selected_row.get_bare_jid()) + elif isinstance(selected_row, Resource): + self.draw_contact_info(selected_row) + self._refresh() |