From f4d4a205f136c6373c415657dd35b4f078f25c69 Mon Sep 17 00:00:00 2001 From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13" Date: Mon, 15 Nov 2010 11:59:09 +0000 Subject: a few renamings, and some other stuff --- CHANGELOG | 13 +- README | 29 +- data/default_config.cfg | 11 +- data/poezio.1 | 7 +- launch.sh | 1 - src/buffers.py | 1331 +++++++++++++++++++++++++++++++++++++++++++++++ src/contact.py | 14 +- src/core.py | 4 +- src/handler.py | 83 --- src/logger.py | 26 +- src/message.py | 4 + src/multiuserchat.py | 2 +- src/singleton.py | 23 - src/tab.py | 70 +-- src/text_buffer.py | 4 + src/user.py | 6 + src/window.py | 1323 ---------------------------------------------- 17 files changed, 1423 insertions(+), 1528 deletions(-) create mode 100644 src/buffers.py delete mode 100644 src/handler.py delete mode 100644 src/singleton.py delete mode 100644 src/window.py diff --git a/CHANGELOG b/CHANGELOG index e033a32c..c0eee2b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,15 +3,20 @@ For more detailed changelog, see the roadmap: http://codingteam.net/project/poezio/roadmap * Poezio 0.7 - dev -"Complete the MUC support" -- All poezio colors can be changed with a theme file -- /say command +"Roster" +- Introduce the roster +- One to one conversations +- Roster search +- Resizable mini-buffer displaying various informations +- All colors can be changed with a theme file +- /say and // commands - Warn user about publicly logged rooms - Possibility to limit the number of history messages received from MUC - auto-rejoin when kicked from a MUC - The number of lines available to scroll down is displayed - Possibility to use a modified nickname automatically when a nick is reserved -- A line indicates the new messages in the window +- A line separates the alread-read messages + from the new messages in a conversation - Information messages are more colored - bugfixes diff --git a/README b/README index 834c14ec..89667b19 100644 --- a/README +++ b/README @@ -10,24 +10,27 @@ Homepage: http://poezio.eu Forge Page: http://codingteam.net/projet/poezio -Poezio is a console Jabber/XMPP client. Its goal is to use anonymous +Poezio is a console Jabber/XMPP client. Its goal is to use anonymous connections to simply let the user join MultiUserChats. This way, the user doesn't have to create a Jabber account, exactly like people are using -IRC. Poezio's commands are designed to be (if possible) like IRC +IRC. Poezio's commands are designed to be (if possible) like IRC clients (weechat, irssi, etc). Since version 0.7, poezio can handle real Jabber accounts along with -roster and one to one conversation, making it a full-featured console +roster and one to one conversations, making it a full-featured console Jabber client, but still MultiUserChats-centered. +In the futur, poezio should implement at a 100% level all XEP related to +MUCs, especially XEP 0045. +All other IM-related XEP (wherever possible) should be implemented through +plugins or directly in poezio's core. ======================= Install ======================= You need python 3.0 or higher, and the SleekXMPP python library. -You can find my patched version at http://github.com/louiz/SleekXMPP until -my changes (required to properly run poezio) are merged upstream. +SleekXMPP can be found at http://github.com/fritzy/SleekXMPP. you can launch poezio with -sh launch.sh +./launch.sh or you can install it with (as root or with sudo) make install @@ -36,7 +39,7 @@ you can now simply launch `poezio' You can edit the config file (~/.config/poezio/poezio.cfg by default) or data/default_config.cfg (if you want to edit the config before the -first launch) +first launch). The default config file is fully commented. Please, see the online documentation for more information on installing, configuring or using poezio: @@ -66,10 +69,10 @@ Report a bug: http://codingteam.net/project/poezio/bugs/add Poezio is Free Software. (learn more: http://www.gnu.org/philosophy/free-sw.html) -Poezio is released under the Gnu GPLv3 license -Please read the COPYING file for details +Poezio is released under the Gnu GPLv3 license. +Please read the COPYING file for details. -The artwork logo is made by Gaëtan Ribémont and released under +The artwork logo was made by Gaëtan Ribémont and released under the Creative Commons BY license (http://creativecommons.org/licenses/by/2.0/) ======================= @@ -85,9 +88,3 @@ the Creative Commons BY license (http://creativecommons.org/licenses/by/2.0/) FlashCode (weechat dev) - Useful advices on how to use ncurses efficiently = Project = Gajim - send_vcard method and common.py - -======================= - Donate -======================= -If you're willing to thank me, or ask for an on-demand feature, please -see the page http://louiz.org/donate.html and contact me. diff --git a/data/default_config.cfg b/data/default_config.cfg index 5d1ddd5e..aaa607b2 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -17,7 +17,7 @@ port = 5222 resource = # the nick you will use when joining a room with no associated nick -# If this is empty, the $USER variable will be used +# If this is empty, the $USER environn +# +# 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 . + +""" +Define all the buffers. +A buffer is a little part of the screen, for example the input buffer, +the text bufferr, the roster buffer, etc. +A Tab (see tab.py) is composed of multiple Buffers +A buffer can also be called Window, even if it's not prefered. +""" + +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 buffer 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.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()) + log.debug('do_command returns : %s\n' % res) + return res + + def success(self): + """ + call the success callback, passing the text as argument + """ + self.on_input = None + log.debug('before on_success') + res = self.on_success(self.get_text()) + log.debug('after on_success, res: %s'%res) + 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() diff --git a/src/contact.py b/src/contact.py index 1eb41f72..8341d4b8 100644 --- a/src/contact.py +++ b/src/contact.py @@ -15,12 +15,15 @@ # along with Poezio. If not, see . """ -Defines the Resource and Contact classes +Defines the Resource and Contact classes, which are used in +the roster """ -from sleekxmpp.xmlstream.stanzabase import JID + import logging log = logging.getLogger(__name__) +from sleekxmpp.xmlstream.stanzabase import JID + class Resource(object): """ Defines a roster item. @@ -80,8 +83,7 @@ class Contact(object): def get_highest_priority_resource(self): """ - There must be, at any time, at least ONE resource. - And they always should be ordered by priority. + Return the resource with the highest priority """ ret = None for resource in self._resources: @@ -94,8 +96,10 @@ class Contact(object): Called, for example, when a new resource get offline (the first, or any subsequent one) """ - # TODO sort by priority + def f(o): + return o.get_priority() self._resources.append(resource) + self._resources = sorted(self._resources, key=f, reverse=True) def remove_resource(self, resource): """ diff --git a/src/core.py b/src/core.py index 40d727b4..ce271613 100644 --- a/src/core.py +++ b/src/core.py @@ -38,9 +38,9 @@ log = logging.getLogger(__name__) import multiuserchat as muc from connection import connection -from handler import Handler from config import config from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab +from logger import logger from user import User from room import Room from roster import Roster, RosterGroup, roster @@ -822,6 +822,8 @@ class Core(object): body = message['body'] if body: date = date if delayed == True else None + if not delayed: + logger.groupchat(room_from, nick_from, body) self.add_message_to_text_buffer(room, body, date, nick_from) self.refresh_window() self.doupdate() diff --git a/src/handler.py b/src/handler.py deleted file mode 100644 index 350ea0ed..00000000 --- a/src/handler.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2009, 2010 Erwan Briand -# Copyright 2010, Florent Le Coz - -# This program 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. - -# This program 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 this program. If not, see . - -from singleton import Singleton - -#Todo, it's not a singleton. Oh, also, remove-me - -class Handler(Singleton): - """ - This class is the global handler for the software's signals. - """ - __is_first_instance = True - - def __init__(self): - if Handler.__is_first_instance: - Handler.__is_first_instance = False - - self.__signals__ = { - - 'on-connected': list(), - # At the end of a successful connection process. - # emitted when presence confirmation is received - # Args: jid - - 'join-room': list(), - # Join a room. - # Args: room, nick - - 'room-presence': list(), - # A presence is received - # Args: the stanza object - - 'room-message': list(), - # A message is received - # Args: the stanza object - - 'private-message': list(), - # A message is received - # Args: the stanza object - - 'room-delayed-message': list(), - # A message is received - # Args: the stanza object - - 'send-version': list(), - # We send our version - # Args: the stanza we reply to - - 'send-time': list(), - # We send our time - # Args: the stanza we reply to - - 'error-message': list(), - # We send our time - # Args: the stanza we reply to - - 'error': list() - # We send our time - # Args: the stanza we reply to - } - - def connect(self, signal, func): - """Connect a function to a signal.""" - if func not in self.__signals__[signal]: - self.__signals__[signal].append(func) - - def emit(self, signal, **kwargs): - """Emit a signal.""" - if signal in self.__signals__: - for func in self.__signals__[signal]: - func(**kwargs) diff --git a/src/logger.py b/src/logger.py index 4cfb7793..a1479f9c 100644 --- a/src/logger.py +++ b/src/logger.py @@ -29,32 +29,10 @@ class Logger(object): """ def __init__(self):# , logfile, loglevel): self.logfile = config.get('logfile', 'logs') - self.loglevel = config.get('loglevel', 3) - # self.logfile = logfile - # self.loglevel = loglevel - def info(self, msg): - if self.logfile and self.loglevel >= 3: - fd = open(self.logfile, 'a') - fd.write(datetime.now().strftime("%H:%M:%S") + ' Info [' + msg + ']\n') - fd.close() - - def warning(self, msg): - if self.logfile and self.loglevel >= 2: - fd = open(self.logfile, 'a') - fd.write(datetime.now().strftime("%H:%M:%S") + ' Warning [' + msg + ']\n') - fd.close() - - def error(self, msg): - if self.logfile and self.loglevel >= 1: - fd = open(self.logfile, 'a') - fd.write(datetime.now().strftime("%H:%M:%S") + ' Error [' + msg + ']\n') - fd.close() - sys.exit(-1) - - def message(self, room, nick, msg): + def groupchat(self, room, nick, msg): """ - log the message in the appropriate room + log the message in the appropriate room's file """ if config.get('use_log', 'false') == 'false': return diff --git a/src/message.py b/src/message.py index 07526819..6cf7a3d3 100644 --- a/src/message.py +++ b/src/message.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Poezio. If not, see . +""" +Define the Message class +""" + from datetime import datetime class Message(object): diff --git a/src/multiuserchat.py b/src/multiuserchat.py index 3927fc38..0fc5cb3c 100644 --- a/src/multiuserchat.py +++ b/src/multiuserchat.py @@ -46,7 +46,7 @@ def change_show(xmpp, jid, own_nick, show, status): Change our 'Show' """ pres = xmpp.makePresence(pto='%s/%s' % (jid, own_nick)) - if show: # if show is None, don't put a tag. It means "online" + if show: # if show is None, don't put a tag. It means "available" pres['type'] = show if status: pres['status'] = status diff --git a/src/singleton.py b/src/singleton.py deleted file mode 100644 index 688d20f0..00000000 --- a/src/singleton.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2009, 2010 Erwan Briand -# Copyright 2010, Florent Le Coz - -# This program 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. -# -# This program 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 this program. If not, see . - -class Singleton(object): - """ Canonic Singleton implementation. - """ - _instance = None - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs) - return cls._instance diff --git a/src/tab.py b/src/tab.py index 5086735b..bef42be8 100644 --- a/src/tab.py +++ b/src/tab.py @@ -15,11 +15,11 @@ # along with Poezio. If not, see . """ -a Tab object is a way to organize various Window (see window.py) +a Tab object is a way to organize various Buffers (see buffers.py) around the screen at once. -A tab is then composed of multiple Window. -Each Tab object has different refresh() and resize() methods, defining of its -Window are displayed, etc +A tab is then composed of multiple Buffer. +Each Tab object has different refresh() and resize() methods, defining how its +Buffer are displayed, resized, etc """ MIN_WIDTH = 50 @@ -28,7 +28,7 @@ MIN_HEIGHT = 16 import logging log = logging.getLogger(__name__) -import window +import buffers import theme import curses import difflib @@ -145,9 +145,9 @@ class InfoTab(Tab): """ def __init__(self, stdscr, core, name): Tab.__init__(self, stdscr, core) - self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.text_win = window.TextWin(self.height-2, self.width, 0, 0, stdscr, self.visible) - self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible) + self.tab_win = buffers.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) + self.text_win = buffers.TextWin(self.height-2, self.width, 0, 0, stdscr, self.visible) + self.input = buffers.Input(1, self.width, self.height-1, 0, stdscr, self.visible) self.name = name self.color_state = theme.COLOR_TAB_NORMAL @@ -209,14 +209,14 @@ class MucTab(Tab): """ Tab.__init__(self, stdscr, core) self._room = room - self.topic_win = window.Topic(1, self.width, 0, 0, stdscr, self.visible) - self.text_win = window.TextWin(self.height-4-self.core.information_win_size, (self.width//10)*9, 1, 0, stdscr, self.visible) - self.v_separator = window.VerticalSeparator(self.height-3, 1, 1, 9*(self.width//10), stdscr, self.visible) - self.user_win = window.UserList(self.height-3, (self.width//10), 1, 9*(self.width//10)+1, stdscr, self.visible) - self.info_header = window.MucInfoWin(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) - self.info_win = window.TextWin(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) - self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) + self.topic_win = buffers.Topic(1, self.width, 0, 0, stdscr, self.visible) + self.text_win = buffers.TextWin(self.height-4-self.core.information_win_size, (self.width//10)*9, 1, 0, stdscr, self.visible) + self.v_separator = buffers.VerticalSeparator(self.height-3, 1, 1, 9*(self.width//10), stdscr, self.visible) + self.user_win = buffers.UserList(self.height-3, (self.width//10), 1, 9*(self.width//10)+1, stdscr, self.visible) + self.info_header = buffers.MucInfoWin(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) + self.info_win = buffers.TextWin(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) + self.tab_win = buffers.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) + self.input = buffers.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): """ @@ -333,11 +333,11 @@ class PrivateTab(Tab): def __init__(self, stdscr, core, room): Tab.__init__(self, stdscr, core) self._room = room - self.text_win = window.TextWin(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible) - self.info_header = window.PrivateInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) - self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) - self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) + self.text_win = buffers.TextWin(self.height-3-self.core.information_win_size, self.width, 0, 0, stdscr, self.visible) + self.info_header = buffers.PrivateInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) + self.info_win = buffers.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) + self.tab_win = buffers.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) + self.input = buffers.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): Tab.resize(self, stdscr) @@ -420,12 +420,12 @@ class RosterInfoTab(Tab): self.name = "Roster" roster_width = self.width//2 info_width = self.width-roster_width-1 - self.v_separator = window.VerticalSeparator(self.height-2, 1, 0, roster_width, stdscr, self.visible) - self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.info_win = window.TextWin(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible) - self.roster_win = window.RosterWin(self.height-2-3, roster_width, 0, 0, stdscr, self.visible) - self.contact_info_win = window.ContactInfoWin(3, roster_width, self.height-2-3, 0, stdscr, self.visible) - self.default_help_message = window.HelpText(1, self.width, self.height-1, 0, stdscr, self.visible, "Enter commands with “/”. “o”: toggle offline show") + self.v_separator = buffers.VerticalSeparator(self.height-2, 1, 0, roster_width, stdscr, self.visible) + self.tab_win = buffers.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) + self.info_win = buffers.TextWin(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible) + self.roster_win = buffers.RosterWin(self.height-2-3, roster_width, 0, 0, stdscr, self.visible) + self.contact_info_win = buffers.ContactInfoWin(3, roster_width, self.height-2-3, 0, stdscr, self.visible) + self.default_help_message = buffers.HelpText(1, self.width, self.height-1, 0, stdscr, self.visible, "Enter commands with “/”. “o”: toggle offline show") self.input = self.default_help_message self.set_color_state(theme.COLOR_TAB_NORMAL) @@ -491,7 +491,7 @@ class RosterInfoTab(Tab): '/' is pressed, we enter "input mode" """ curses.curs_set(1) - self.input = window.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "", self.reset_help_message, self.execute_slash_command) + self.input = buffers.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "", self.reset_help_message, self.execute_slash_command) self.input.do_command("/") # we add the slash def reset_help_message(self, _=None): @@ -551,7 +551,7 @@ class RosterInfoTab(Tab): in it. """ curses.curs_set(1) - self.input = window.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) + self.input = buffers.CommandInput(1, self.width, self.height-1, 0, self.default_help_message, self.visible, "[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) return True def set_roster_filter(self, txt): @@ -578,12 +578,12 @@ class ConversationTab(Tab): self._text_buffer = text_buffer self.color_state = theme.COLOR_TAB_NORMAL self._name = jid # a conversation tab is linked to one specific full jid OR bare jid - self.text_win = window.TextWin(self.height-4-self.core.information_win_size, self.width, 1, 0, stdscr, self.visible) - self.upper_bar = window.ConversationStatusMessageWin(1, self.width, 0, 0, stdscr, self.visible) - self.info_header = window.ConversationInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) - self.info_win = window.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) - self.tab_win = window.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) - self.input = window.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) + self.text_win = buffers.TextWin(self.height-4-self.core.information_win_size, self.width, 1, 0, stdscr, self.visible) + self.upper_bar = buffers.ConversationStatusMessageWin(1, self.width, 0, 0, stdscr, self.visible) + self.info_header = buffers.ConversationInfoWin(1, self.width, self.height-3-self.core.information_win_size, 0, stdscr, self.visible) + self.info_win = buffers.TextWin(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, stdscr, self.visible) + self.tab_win = buffers.GlobalInfoBar(1, self.width, self.height-2, 0, stdscr, self.visible) + self.input = buffers.MessageInput(1, self.width, self.height-1, 0, stdscr, self.visible) def resize(self, stdscr): Tab.resize(self, stdscr) diff --git a/src/text_buffer.py b/src/text_buffer.py index 203a966e..43bf008f 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Poezio. If not, see . +""" +Define the TextBuffer class +""" + from message import Message from datetime import datetime import theme diff --git a/src/user.py b/src/user.py index eb8587f8..ba39a8fb 100644 --- a/src/user.py +++ b/src/user.py @@ -14,6 +14,11 @@ # You should have received a copy of the GNU General Public License # along with Poezio. If not, see . +""" +Define the user class. +An user is a MUC participant, not a roster contact (see contact.py) +""" + from random import randrange, choice from config import config from datetime import timedelta, datetime @@ -26,6 +31,7 @@ ROLE_DICT = { 'participant':2, 'moderator':3 } + class User(object): """ keep trace of an user in a Room diff --git a/src/window.py b/src/window.py deleted file mode 100644 index db8d2822..00000000 --- a/src/window.py +++ /dev/null @@ -1,1323 +0,0 @@ -# Copyright 2010 Le Coz Florent -# -# 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 . - -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 buffer 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.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()) - log.debug('do_command returns : %s\n' % res) - return res - - def success(self): - """ - call the success callback, passing the text as argument - """ - self.on_input = None - log.debug('before on_success') - res = self.on_success(self.get_text()) - log.debug('after on_success, res: %s'%res) - 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() -- cgit v1.2.3