diff options
-rw-r--r-- | CHANGELOG | 5 | ||||
-rw-r--r-- | data/default_config.cfg | 20 | ||||
-rw-r--r-- | src/common.py | 8 | ||||
-rw-r--r-- | src/connection.py | 1 | ||||
-rw-r--r-- | src/core.py | 86 | ||||
-rw-r--r-- | src/logger.py | 10 | ||||
-rw-r--r-- | src/pubsub.py | 307 | ||||
-rw-r--r-- | src/room.py | 9 | ||||
-rw-r--r-- | src/roster.py | 4 | ||||
-rw-r--r-- | src/tabs.py | 36 | ||||
-rw-r--r-- | src/text_buffer.py | 2 | ||||
-rw-r--r-- | src/windows.py | 33 |
12 files changed, 465 insertions, 56 deletions
@@ -2,18 +2,21 @@ This file describes the new features in each poezio release. For more detailed changelog, see the roadmap: http://dev.louiz.org/project/poezio/roadmap + * Poezio 0.7.2 - dev - Chatstate notifications (in private AND in MUCs) - /message command to talk to any JID - /version command to get the software version of an entity -- +- /bind command, and keys can be bound in the config file - Multiline edition + * Poezio 0.7.1 - 2 Feb 2010 - /status command to globally change the status - /win command now accepts part of tab name as argument - bugfixes + * Poezio 0.7 - 14 jan 2010 Codename ”Koshie & Mathieui” - Library changed from xmpppy to SleekXMPP diff --git a/data/default_config.cfg b/data/default_config.cfg index c376a73c..53357009 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -136,6 +136,16 @@ log_dir = # with no activity, set to true. Else, set to false show_inactive_tabs = true +# The terminal can beep on various event. Put the event you want in a list +# (separated by spaces). +# The events can be +# - highlight (when you are highlighted in a MUC) +# - private (when a new private message is received, from your contacts or +# someone from a MUC) +# - message (any message from a MUC) +beep_on = highlight private + + # Theme # If themes_dir is not set, logs will searched for in $XDG_DATA_HOME/poezio/themes, @@ -179,6 +189,16 @@ send_time = true max_messages_in_memory = 2048 max_lines_in_memory = 2048 +[bindings] +# Bindings are keyboard shortcut aliases. You can use them +# to define your own keys and bind them with some functions +# The syntaxe is +# key = bind +# where ^x means Control + x +# and M-x means Alt + x +# The example turns Alt + i into a tab key +M-i = ^I + [var] # You should not edit this section, it is just used by poezio # to save various data across restarts diff --git a/src/common.py b/src/common.py index 9435dab5..0bc93c8d 100644 --- a/src/common.py +++ b/src/common.py @@ -42,6 +42,8 @@ import curses import time import shlex +from config import config + ROOM_STATE_NONE = 11 ROOM_STATE_CURRENT = 10 ROOM_STATE_PRIVATE = 15 @@ -211,3 +213,9 @@ def curses_color_pair(color): if color < 0: return curses.color_pair(-color) | curses.A_BOLD return curses.color_pair(color) + +def replace_key_with_bound(key): + if config.has_option('bindings', key): + return config.get(key, key, 'bindings') + else: + return key diff --git a/src/connection.py b/src/connection.py index 8bef6eb2..d021f44b 100644 --- a/src/connection.py +++ b/src/connection.py @@ -56,6 +56,7 @@ class Connection(sleekxmpp.ClientXMPP): self.register_plugin('xep_0030') self.register_plugin('xep_0004') self.register_plugin('xep_0045') + self.register_plugin('xep_0060') self.register_plugin('xep_0071') self.register_plugin('xep_0085') if config.get('send_poezio_info', 'true') == 'true': diff --git a/src/core.py b/src/core.py index 4aa1b2b3..df7f3cad 100644 --- a/src/core.py +++ b/src/core.py @@ -129,17 +129,16 @@ class Core(object): 'version': (self.command_version, _('Usage: /version <jid>\nVersion: get the software version of the given JID (usually its XMPP client and Operating System)'), None), 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: disconnect from the remote server if you are currently connected and then connect to it again'), None), 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None), + 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same than the Up key.')), } self.key_func = { "KEY_PPAGE": self.scroll_page_up, "KEY_NPAGE": self.scroll_page_down, "KEY_F(5)": self.rotate_rooms_left, - "M-[1;6D": self.rotate_rooms_left, "^P": self.rotate_rooms_left, 'kLFT3': self.rotate_rooms_left, "KEY_F(6)": self.rotate_rooms_right, - "M-[1;6C": self.rotate_rooms_right, "^N": self.rotate_rooms_right, 'kRIT3': self.rotate_rooms_right, "KEY_F(7)": self.shrink_information_win, @@ -335,6 +334,7 @@ class Core(object): def on_got_offline(self, presence): jid = presence['from'] contact = roster.get_contact_by_jid(jid.bare) + logger.log_roster_change(jid.bare, 'got offline') if not contact: return log.debug('on_got_offline: %s' % presence) @@ -356,6 +356,7 @@ class Core(object): if not contact: # Todo, handle presence comming from contacts not in roster return + logger.log_roster_change(jid.bare, 'got online') resource = contact.get_resource_by_fulljid(jid.full) assert not resource resource = Resource(jid.full) @@ -485,7 +486,7 @@ class Core(object): for tab in self.tabs: if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab): if message['type'] == 'error': - return self.room_error(message, tab.get_room().name) + return self.room_error(message, jid_from) else: return self.on_groupchat_private_message(message) return self.on_normal_message(message) @@ -513,6 +514,8 @@ class Core(object): conversation.remote_wants_chatstates = True else: conversation.remote_wants_chatstates = False + if 'private' in config.get('beep_on', 'highlight private').split(): + curses.beep() logger.log_message(jid.full.replace('/', '\\'), nick_from, body) if conversation is self.current_tab(): self.refresh_window() @@ -550,6 +553,8 @@ class Core(object): jid = message['from'] body = xhtml.get_body_from_message_stanza(message) if not body: + if message['type'] == 'error': + self.information(self.get_error_message_from_error_stanza(message), 'Error') return conversation = self.get_tab_of_conversation_with_jid(jid, create=True) if roster.get_contact_by_jid(jid.bare): @@ -563,6 +568,8 @@ class Core(object): else: conversation.remote_wants_chatstates = False logger.log_message(jid.bare, remote_nick, body) + if 'private' in config.get('beep_on', 'highlight private').split(): + curses.beep() if self.current_tab() is not conversation: conversation.set_color_state(theme.COLOR_TAB_PRIVATE) self.refresh_tab_win() @@ -668,7 +675,8 @@ class Core(object): """ # curses.ungetch(0) # FIXME while self.running: - char_list = self.read_keyboard() + char_list = [common.replace_key_with_bound(key)\ + for key in self.read_keyboard()] # Special case for M-x where x is a number if len(char_list) == 1: char = char_list[0] @@ -767,7 +775,8 @@ class Core(object): def refresh_tab_win(self): self.current_tab().tab_win.refresh() - self.current_tab().input.refresh() + if self.current_tab().input: + self.current_tab().input.refresh() self.doupdate() def add_tab(self, new_tab, focus=False): @@ -850,26 +859,34 @@ class Core(object): self.current_tab().on_scroll_up() self.refresh_window() - def room_error(self, error, room_name): + def get_error_message_from_error_stanza(self, stanza): """ - Display the error on the room window + Takes a stanza of the form <message type='error'><error/></message> + and return a well formed string containing the error informations """ - room = self.get_room_by_name(room_name) - msg = error['error']['type'] - condition = error['error']['condition'] - code = error['error']['code'] - body = error['error']['text'] + msg = stanza['error']['type'] + condition = stanza['error']['condition'] + code = stanza['error']['code'] + body = stanza['error']['text'] if not body: if code in ERROR_AND_STATUS_CODES: body = ERROR_AND_STATUS_CODES[code] else: body = condition or _('Unknown error') if code: - msg = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code} - self.add_message_to_text_buffer(room, msg) + message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code} else: - msg = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body} - self.add_message_to_text_buffer(room, msg) + message = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body} + return message + + def room_error(self, error, room_name): + """ + Display the error on the room window + """ + room = self.get_room_by_name(room_name) + error_message = self.get_error_message_from_error_stanza(error) + self.add_message_to_text_buffer(room, error_message) + code = error['error']['code'] if code == '401': msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') self.add_message_to_text_buffer(room, msg) @@ -968,13 +985,15 @@ class Core(object): body = xhtml.get_body_from_message_stanza(message) if body: date = date if delayed == True else None - self.add_message_to_text_buffer(room, body, date, nick_from) + self.add_message_to_text_buffer(room, body, date, nick_from, history=True if date else False) if tab is self.current_tab(): tab.text_win.refresh(tab._room) tab.info_header.refresh(tab._room, tab.text_win) self.refresh_tab_win() + if 'message' in config.get('beep_on', 'highlight private').split(): + curses.beep() - def add_message_to_text_buffer(self, room, txt, time=None, nickname=None): + def add_message_to_text_buffer(self, room, txt, time=None, nickname=None, history=None): """ Add the message to the room if possible, else, add it to the Info window (in the Info tab of the info window in the RosterTab) @@ -982,7 +1001,7 @@ class Core(object): if not room: self.information('Trying to add a message in no room: %s' % txt, 'Error') else: - room.add_message(txt, time, nickname) + room.add_message(txt, time, nickname, history=history) def command_help(self, arg): """ @@ -1073,7 +1092,7 @@ class Core(object): """ /reconnect """ - self.disconnect(True) + self.disconnect(reconnect=True) def command_list(self, arg): """ @@ -1221,6 +1240,7 @@ class Core(object): else: # no server could be found, print a message and return self.information(_("You didn't specify a server for the room you want to join"), 'Error') return + room = room.lower() r = self.get_room_by_name(room) if len(args) == 2: # a password is provided password = args[1] @@ -1229,7 +1249,6 @@ class Core(object): return if room.startswith('@'): room = room[1:] - room = room.lower() current_status = self.get_status() if r and not r.joined: muc.join_groupchat(self.xmpp, room, nick, password, @@ -1359,6 +1378,16 @@ class Core(object): tab.get_room().joined = False self.command_join(tab.get_name()) + def command_bind(self, arg): + """ + Bind a key. + """ + args = common.shell_split(arg) + if len(args) != 2: + return self.command_help('bind') + config.set_and_save(args[0], args[1], section='bindings') + self.information('%s is now bound to %s' % (args[0], args[1]), 'Info') + def go_to_room_number(self): """ Read 2 more chars and go to the tab @@ -1378,15 +1407,19 @@ class Core(object): def information(self, msg, typ=''): """ - Displays an informational message in the "Info" room window + Displays an informational message in the "Info" buffer """ nb_lines = self.information_buffer.add_message(msg, nickname=typ) if typ != '' and typ.lower() in config.get('information_buffer_popup_on', 'error roster warning help info').split(): popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2 self.pop_information_win_up(nb_lines, popup_time) + else: + if self.information_win_size != 0: + self.information_win.refresh(self.information_buffer) + self.current_tab().input.refresh() - def disconnect(self, msg=None): + def disconnect(self, msg=None, reconnect=False): """ Disconnect from remote server and correctly set the states of all parts of the client (for example, set the MucTabs as not joined, etc) @@ -1394,13 +1427,10 @@ class Core(object): for tab in self.tabs: if isinstance(tab, tabs.MucTab): muc.leave_groupchat(self.xmpp, tab.get_room().name, tab.get_room().own_nick, msg) + roster.empty() self.save_config() # Ugly fix thanks to gmail servers - try: - sys.stderr = None - self.xmpp.disconnect(False) - except: - pass + self.xmpp.disconnect(reconnect) def command_quit(self, arg): """ diff --git a/src/logger.py b/src/logger.py index ad615f9b..d87eaa6b 100644 --- a/src/logger.py +++ b/src/logger.py @@ -34,6 +34,7 @@ class Logger(object): """ def __init__(self): self.logfile = config.get('logfile', 'logs') + self.roster_logfile = None # a dict of 'groupchatname': file-object (opened) self.fds = dict() @@ -81,4 +82,13 @@ class Logger(object): else: fd.flush() # TODO do something better here? + def log_roster_change(self, jid, message): + if not self.roster_logfile: + try: + self.roster_logfile = open(os.path.join(DATA_HOME, 'logs', 'roster.log'), 'a') + except IOError: + return + self.roster_logfile.write('%s %s %s\n' % (datetime.now().strftime('%d-%m-%y [%H:%M:%S]'), jid, message)) + self.roster_logfile.flush() + logger = Logger() diff --git a/src/pubsub.py b/src/pubsub.py new file mode 100644 index 00000000..af9ca001 --- /dev/null +++ b/src/pubsub.py @@ -0,0 +1,307 @@ +# Copyright 2010-2011 Le Coz Florent <louiz@louiz.org> +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see <http://www.gnu.org/licenses/>. + +import logging +log = logging.getLogger(__name__) + +import curses + +import windows +import tabs + +from sleekxmpp.xmlstream import ElementBase, ET + +class PubsubNode(object): + node_type = None # unknown yet + def __init__(self, name, parent=None): + self.items = [] + self.name = name + self.parent = parent + + +class LeafNode(PubsubNode): + node_type = "leaf" + def __init__(self, name, parent=None): + PubsubNode.__init__(self, name, parent) + + +class CollectionNode(PubsubNode): + node_type = "collection" + def __init__(self, name, parent=None): + PubsubNode.__init__(self, name, parent) + self.subnodes = [] + + +class PubsubItem(object): + def __init__(self, idd, content): + self.id = idd + self.content = content + + def to_dict(self, columns): + """ + returns a dict of the values listed in columns + """ + ret = {} + for col in columns: + ret[col] = self.__dict__.get(col) or '' + return ret + +class PubsubBrowserTab(tabs.Tab): + """ + A tab containing a pubsub browser letting the user + list nodes and items, view, add and delete items, etc + """ + def __init__(self, server): + """ + Server is the name of the pubsub server, for example: + pubsub.example.com + All action done in this tab will be made on that server. + """ + tabs.Tab.__init__(self) + self.current_node = None # the subnode we are listing. None means the root + self.server = server + self.nodes = [] # the lower level of nodes + + self.tab_win = windows.GlobalInfoBar() + self.upper_message = windows.Topic() + self.upper_message.set_message('Pubsub server: %s/%s' % (self.server,self.current_node or '')) + + # Node List View + node_columns = ('node', 'name',) + self.node_list_header = windows.ColumnHeaderWin(node_columns) + self.node_listview = windows.ListWin(node_columns) + + # Item List View + item_columns = ('id',) + self.item_list_header = windows.ColumnHeaderWin(item_columns) + self.item_listview = windows.ListWin(item_columns) + + # Item viewer + self.item_viewer = windows.SimpleTextWin('') + self.default_help_message = windows.HelpText("“c”: create a node.") + self.input = self.default_help_message + + self.key_func['c'] = self.command_create_node + self.key_func["M-KEY_DOWN"] = self.scroll_node_down + self.key_func["M-KEY_UP"] = self.scroll_node_up + self.key_func["KEY_DOWN"] = self.item_listview.move_cursor_down + self.key_func["KEY_UP"] = self.item_listview.move_cursor_up + self.key_func["^M"] = self.open_selected_item + self.resize() + + self.get_nodes() + + def resize(self): + self.upper_message.resize(1, self.width, 0, 0) + self.tab_win.resize(1, self.width, self.height-2, 0) + + column_size = {'node': self.width//4, + 'name': self.width//4,} + self.node_list_header.resize_columns(column_size) + self.node_list_header.resize(1, self.width//2, 1, 0) + self.node_listview.resize_columns(column_size) + self.node_listview.resize(self.height//2-2, self.width//2, 2, 0) + + column_size = {'id': self.width//2,} + self.item_list_header.resize_columns(column_size) + self.item_list_header.resize(self.height//2+1, self.width//2, self.height//2, 0) + self.item_listview.resize_columns(column_size) + self.item_listview.resize(self.height//2-3, self.width//2, self.height//2+1, 0) + + self.item_viewer.resize(self.height-3, self.width//2+1, 1, self.width//2) + self.input.resize(1, self.width, self.height-1, 0) + + def refresh(self): + if self.need_resize: + self.resize() + log.debug(' TAB Refresh: %s'%self.__class__.__name__) + self.upper_message.refresh() + self.node_list_header.refresh() + self.node_listview.refresh() + self.item_list_header.refresh() + self.item_listview.refresh() + self.item_viewer.refresh() + self.tab_win.refresh() + self.input.refresh() + + def get_name(self): + return '%s@@pubsubbrowser' % (self.server,) + + def on_input(self, key): + res = self.input.do_command(key) + if res: + return True + if key in self.key_func: + return self.key_func[key]() + + def get_selected_node_name(self): + """ + From the node_view_list, returns the node name of the selected + one. None can be returned + """ + line = self.node_listview.get_selected_row() + if not line: + return None + return line['node'] + + def get_node_by_name(self, name): + """ + in the currently browsed node (or on the root), return the node with that name + """ + nodes = self.current_node and self.current_node.subnodes or self.nodes + for node in nodes: + if node.name == name: + return node + return None + + def get_item_by_id(self, idd): + """ + in the currently selected node, return the item with that id + """ + selected_node_name = self.get_selected_node_name() + if not selected_node_name: + return None + selected_node = self.get_node_by_name(selected_node_name) + if not selected_node: + return None + for item in selected_node.items: + if item.id == idd: + return item + return None + + def get_selected_item_id(self): + """ + returns the id of the currently selected item + """ + line = self.item_listview.get_selected_row() + if not line: + return None + return line['id'] + + def get_items(self, node): + """ + Get all items in the given node + """ + items = self.core.xmpp.plugin['xep_0060'].get_items(self.server, node.name) + item_list = [] + if items: + for it in items: + item_list.append(PubsubItem(it.attrib['id'], it)) + node.items = item_list + log.debug('get_selected_node_name: %s' % self.get_selected_node_name()) + if self.get_selected_node_name() == node.name: + self.display_items_from_node(node) + log.debug('Item on node %s: %s' % (node.name, item_list)) + + def display_items_from_node(self, node): + """ + takes a node, and set fill the item_listview with that + node’s items + """ + columns = self.item_list_header.get_columns() + self.item_listview.empty() + log.debug('display_items_from_node: %s' % node.items) + for item in node.items: + self.item_listview.lines.append(item.to_dict(columns)) + + def add_nodes(self, node_list, parent=None): + """ + Add Node objects to the list of the parent. + If parent is None, they are added to the root list. + If the current selected node is parent, we add + them directly to the node_listview + """ + log.debug('Adding nodes to %s: %s' % (node_list, parent,)) + if not parent: + list_to_append = self.nodes + else: + list_to_append = parent.nodes + self.node_listview.add_lines(node_list) + for node in node_list: + new_node = LeafNode(node['node']) + list_to_append.append(new_node) + self.get_items(new_node) + + def get_nodes(self, node=None): + """ + Get all subnodes of the given node. If no node is given, get + the root nodes + """ + nodes = self.core.xmpp.plugin['xep_0060'].get_nodes(self.server) + lines = [{'name': nodes[node] or '', + 'node': node} for node in nodes.keys()] + self.add_nodes(lines) + + def create_node(self, node_name): + if node_name: + res = self.core.xmpp.plugin['xep_0060'].create_node(self.server, node_name) + if res: + self.node_listview.add_lines([{'name': '', 'node': node_name}]) + self.reset_help_message() + return True + + def reset_help_message(self, txt=None): + """ + Just reset the help message when a command ends + """ + curses.curs_set(0) + self.input = self.default_help_message + return True + + def command_create_node(self): + """ + Prompt for a node name and create it on Enter key + """ + curses.curs_set(1) + self.input = windows.CommandInput("[Create node]", self.reset_help_message, self.create_node, None) + self.input.resize(1, self.width, self.height-1, 0) + return True + + def scroll_node_up(self): + """ + scroll the node up, and update the item list if needed + """ + selected_node_before = self.get_selected_node_name() + self.node_listview.move_cursor_up() + selected_node_after = self.get_selected_node_name() + if selected_node_after is not selected_node_before: + self.display_items_from_node(self.get_node_by_name(selected_node_after)) + return True + return False + + def scroll_node_down(self): + """ + scroll the node down, and update the item list if needed + """ + selected_node_before = self.get_selected_node_name() + self.node_listview.move_cursor_down() + selected_node_after = self.get_selected_node_name() + if selected_node_after is not selected_node_before: + self.display_items_from_node(self.get_node_by_name(selected_node_after)) + return True + return False + + def open_selected_item(self): + """ + displays the currently selected item in the item view window + """ + selected_item = self.get_item_by_id(self.get_selected_item_id()) + if not selected_item: + return + log.debug('Content: %s'%ET.tostring(selected_item.content)) + self.item_viewer._text = str(ET.tostring(selected_item.content)) + self.item_viewer.rebuild_text() + return True diff --git a/src/room.py b/src/room.py index 45ebddbd..5d4c4ce6 100644 --- a/src/room.py +++ b/src/room.py @@ -24,6 +24,7 @@ import common import theme import logging +import curses log = logging.getLogger(__name__) @@ -77,6 +78,10 @@ class Room(TextBuffer): self.set_color_state(theme.COLOR_TAB_HIGHLIGHT) color = theme.COLOR_HIGHLIGHT_NICK break + if color: + beep_on = config.get('beep_on', 'highlight private').split() + if 'highlight' in beep_on and 'message' not in beep_on: + curses.beep() return color def get_user_by_name(self, nick): @@ -95,7 +100,7 @@ class Room(TextBuffer): """ self.color_state = color - def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None): + def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, history=None): """ Note that user can be None even if nickname is not None. It happens when we receive an history message said by someone who is not @@ -130,7 +135,7 @@ class Room(TextBuffer): self.messages.append(message) for window in self.windows: # make the associated windows # build the lines from the new message - nb = window.build_new_message(message) + nb = window.build_new_message(message, history=history) if window.pos != 0: window.scroll_up(nb) return nb diff --git a/src/roster.py b/src/roster.py index aed5f5a0..afe83c9e 100644 --- a/src/roster.py +++ b/src/roster.py @@ -45,6 +45,10 @@ class Roster(object): except IOError: return + def empty(self): + self._contacts = {} + self._roster_groups = [] + def add_contact(self, contact, jid): """ Add a contact to the contact list diff --git a/src/tabs.py b/src/tabs.py index 4ea3c280..8d4e6447 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -269,6 +269,7 @@ class ChatTab(Tab): self.commands['say'] = (self.command_say, _("""Usage: /say <message>\nSay: Just send the message. Useful if you want your message to begin with a '/'"""), None) + self.chat_state = None def last_words_completion(self): """ @@ -308,6 +309,7 @@ class ChatTab(Tab): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = self.message_type msg['chat_state'] = state + self.chat_state = state msg.send() def send_composing_chat_state(self, empty_before, empty_after): @@ -316,8 +318,13 @@ class ChatTab(Tab): on the the current status of the input """ if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates: - if empty_after: + if self.chat_state == "composing" and not empty_after: + self.cancel_paused_delay() + self.set_paused_delay(True) + elif empty_after and not self.chat_state == 'active': + self.cancel_paused_delay() self.send_chat_state("active") + elif empty_after: self.cancel_paused_delay() elif empty_before or (self.timed_event_paused is not None and not self.timed_event_paused()): self.cancel_paused_delay() @@ -388,7 +395,6 @@ class MucTab(ChatTab): self.ignores = [] # set of Users # keys self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion self.key_func['M-u'] = self.scroll_user_list_down self.key_func['M-y'] = self.scroll_user_list_up # commands @@ -912,6 +918,7 @@ class MucTab(ChatTab): if status: leave_msg += ' (%s)' % status room.add_message(leave_msg) + self.core.refresh_window() self.core.on_user_left_private_conversation(from_room, from_nick, status) def on_user_change_status(self, room, user, from_nick, from_room, affiliation, role, show, status): @@ -971,7 +978,6 @@ class PrivateTab(ChatTab): self.input = windows.MessageInput() # keys self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion # commands self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None) @@ -983,7 +989,11 @@ class PrivateTab(ChatTab): def command_say(self, line): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' - msg['body'] = line + if line.find('\x19') == -1: + msg['body'] = line + else: + msg['body'] = xhtml.clean_text(line) + msg['xhtml_im'] = xhtml.poezio_colors_to_html(line) if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False: msg['chat_state'] = 'active' msg.send() @@ -1041,7 +1051,8 @@ class PrivateTab(ChatTab): empty_before = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) self.input.do_command(key) empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - self.send_composing_chat_state(empty_before, empty_after) + if self.core.get_tab_by_name(JID(self.get_room().name).bare, MucTab).get_room().joined: + self.send_composing_chat_state(empty_before, empty_after) return False def on_lose_focus(self): @@ -1054,7 +1065,7 @@ class PrivateTab(ChatTab): def on_gain_focus(self): self._room.set_color_state(theme.COLOR_TAB_CURRENT) curses.curs_set(1) - if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): + if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('active') def on_scroll_up(self): @@ -1089,9 +1100,9 @@ class PrivateTab(ChatTab): The user left the associated MUC """ if not status_message: - self.get_room().add_message(_('%(spec)s "[%(nick)s]" has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}) + self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}) else: - self.get_room().add_message(_('%(spec)s "[%(nick)s]" has left the room "(%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) + self.get_room().add_message(_('\x191%(spec)s \x193%(nick)s\x195 has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) class RosterInfoTab(Tab): """ @@ -1110,7 +1121,6 @@ class RosterInfoTab(Tab): self.input = self.default_help_message self.set_color_state(theme.COLOR_TAB_NORMAL) self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion self.key_func[' '] = self.on_space self.key_func["/"] = self.on_slash self.key_func["KEY_UP"] = self.move_cursor_up @@ -1457,7 +1467,6 @@ class ConversationTab(ChatTab): self.input = windows.MessageInput() # keys self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion # commands self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None) @@ -1469,7 +1478,11 @@ class ConversationTab(ChatTab): def command_say(self, line): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' - msg['body'] = line + if line.find('\x19') == -1: + msg['body'] = line + else: + msg['body'] = xhtml.clean_text(line) + msg['xhtml_im'] = xhtml.poezio_colors_to_html(line) if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False: msg['chat_state'] = 'active' msg.send() @@ -1585,7 +1598,6 @@ class MucListTab(Tab): self.key_func["KEY_DOWN"] = self.listview.move_cursor_down self.key_func["KEY_UP"] = self.listview.move_cursor_up self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion self.key_func["/"] = self.on_slash self.key_func['j'] = self.join_selected self.key_func['J'] = self.join_selected_no_focus diff --git a/src/text_buffer.py b/src/text_buffer.py index e0d0fc1c..a3b5b1fb 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -44,7 +44,7 @@ class TextBuffer(object): def add_window(self, win): self.windows.append(win) - def add_message(self, txt, time=None, nickname=None, nick_color=None): + def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None): msg = Message(txt='%s\x19o'%(txt,), nick_color=nick_color, time=time or datetime.now(), nickname=nickname, user=None) self.messages.append(msg) diff --git a/src/windows.py b/src/windows.py index 1d54f7ed..d4d9b2a7 100644 --- a/src/windows.py +++ b/src/windows.py @@ -538,7 +538,7 @@ class TextWin(Win): if None not in self.built_lines: self.built_lines.append(None) - def build_new_message(self, message): + def build_new_message(self, message, history=None): """ Take one message, build it and add it to the list Return the number of lines that are built for the given @@ -570,7 +570,10 @@ class TextWin(Win): else: txt = txt.replace('\t', ' ') # length of the time - offset = 9 + if history: + offset = 20 + else: + offset = 9 if theme.CHAR_TIME_RIGHT: offset += 1 if theme.CHAR_TIME_RIGHT: @@ -596,7 +599,10 @@ class TextWin(Win): else: color = None if first: - time = message.time.strftime("%H:%M:%S") + if history: + time = message.time.strftime("%Y-%m-%d %H:%M:%S") + else: + time = message.time.strftime("%H:%M:%S") nickname = nick else: time = None @@ -1326,7 +1332,7 @@ class RosterWin(Win): self.roster_len = len(roster) while self.roster_len and self.pos >= self.roster_len: self.move_cursor_up() - # self._win.erase() + self._win.erase() self._win.move(0, 0) self.draw_roster_information(roster) y = 1 @@ -1363,10 +1369,6 @@ class RosterWin(Win): y += 1 if y-self.start_pos+1 == self.height: break - line = ' '*self.width - while y < self.height: - self.addstr(y, 0, line) - y += 1 if self.start_pos > 1: self.draw_plus(1) if self.start_pos + self.height-2 < self.roster_len: @@ -1516,6 +1518,14 @@ class ListWin(Win): self._selected_row = 0 self._starting_pos = 0 # The column number from which we start the refresh + def empty(self): + """ + emtpy the list and reset some important values as well + """ + self.lines = [] + self._selected_row = 0 + self._starting_pos = 0 + def resize_columns(self, dic): """ Resize the width of the columns @@ -1607,6 +1617,9 @@ class ColumnHeaderWin(Win): def resize_columns(self, dic): self._columns_sizes = dic + def get_columns(self): + return self._columns + def refresh(self): log.debug('Refresh: %s'%self.__class__.__name__) with g_lock: @@ -1626,10 +1639,6 @@ class SimpleTextWin(Win): self._text = text self.built_lines = [] - def resize(self, height, width, y, x, stdscr): - self._resize(height, width, y, x, stdscr) - self.rebuild_text() - def rebuild_text(self): """ Transform the text in lines than can then be |