diff options
author | Florent Le Coz <louiz@louiz.org> | 2012-05-20 13:43:53 +0200 |
---|---|---|
committer | Florent Le Coz <louiz@louiz.org> | 2012-05-20 13:43:53 +0200 |
commit | 65c2d3dc8891eae4307375eef4b79d13ac793e46 (patch) | |
tree | 98359af6c6d9809585b52dcc1aadf083fa590412 | |
parent | 0f0efb7ada6ad60f88360bc6a8feb3607fcabd3c (diff) | |
parent | 51c788ad96703d215942499ffefe6fdc98326b6b (diff) | |
download | poezio-65c2d3dc8891eae4307375eef4b79d13ac793e46.tar.gz poezio-65c2d3dc8891eae4307375eef4b79d13ac793e46.tar.bz2 poezio-65c2d3dc8891eae4307375eef4b79d13ac793e46.tar.xz poezio-65c2d3dc8891eae4307375eef4b79d13ac793e46.zip |
Merge branch 'master' of https://git.louiz.org/poezio
-rw-r--r-- | data/default_config.cfg | 19 | ||||
-rw-r--r-- | doc/en/configure.txt | 51 | ||||
-rw-r--r-- | doc/en/index.txt | 17 | ||||
-rw-r--r-- | doc/en/keys.txt | 5 | ||||
-rw-r--r-- | doc/en/plugins.txt | 6 | ||||
-rw-r--r-- | doc/en/plugins/otr.txt | 36 | ||||
-rw-r--r-- | doc/en/plugins/simple_notify.txt | 4 | ||||
-rw-r--r-- | plugins/alias.py | 17 | ||||
-rw-r--r-- | plugins/link.py | 7 | ||||
-rw-r--r-- | plugins/otr.py | 2 | ||||
-rw-r--r-- | plugins/quote.py | 8 | ||||
-rw-r--r-- | src/core.py | 105 | ||||
-rw-r--r-- | src/events.py | 6 | ||||
-rw-r--r-- | src/roster.py | 6 | ||||
-rw-r--r-- | src/tabs.py | 99 | ||||
-rw-r--r-- | src/text_buffer.py | 6 | ||||
-rw-r--r-- | src/theming.py | 3 | ||||
-rw-r--r-- | src/windows.py | 104 | ||||
-rw-r--r-- | src/xhtml.py | 5 |
19 files changed, 431 insertions, 75 deletions
diff --git a/data/default_config.cfg b/data/default_config.cfg index 2c5f0d81..61c30c21 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -24,7 +24,6 @@ ignore_certificate = false # value to the services default. whitespace_interval = 300 - # Path to the certificate authenticating the Authority # A server may have several certificates, but if it uses a CA, it will often # keep the same for obvious reasons, so this is a good option if your server @@ -221,6 +220,9 @@ show_muc_jid = true # poezio will only show: toto (2) show_roster_jids = true +# If set to true, the roster will display the offline contacts too +roster_show_offline = false + # The terminal can beep on various event. Put the event you want in a list # (separated by spaces). # The events can be @@ -261,7 +263,6 @@ vertical_tab_list_sort = desc # possible values: desc, asc user_list_sort = desc - # The nick of people who join, part, change their status, etc. in a MUC will # be displayed using their nick color if true. display_user_color_in_join_part = false @@ -295,6 +296,20 @@ send_time = true max_messages_in_memory = 2048 max_lines_in_memory = 2048 +# Show the separator at the bottom of the text buffer, even if no one +# spoke +show_useless_separator = false + +# Set this to true if you want the commands to be executed remotely +# (with ssh & the daemon), see the documentation of the /link plugin +# for details +exec_remote = false + +# Path of the FIFO in which the remote commands will be sent. +# Used with exec_remote set to true, see the documentation of /link for details +# Defaults to ./poezio.fifo +remote_fifo_path = + # Defines if all tabs are resized at the same time (if set to false) # or if they are really resized only when needed (if set to true). # “true” should be the most comfortable value diff --git a/doc/en/configure.txt b/doc/en/configure.txt index 36a49206..e5098da1 100644 --- a/doc/en/configure.txt +++ b/doc/en/configure.txt @@ -275,6 +275,10 @@ section of this documentation. the contact names). If there is no contact name, the JID will still be displayed. +*roster_show_offline*:: false + + Set this to true if you want to display the offline contacts too. + *beep_on*:: highlight private The terminal can beep on various event. Put the event you want in a list @@ -387,6 +391,17 @@ section of this documentation. You can specify another directory to use. It will be created if it does not exist. +*exec_remote*:: false + + If this is set to true, poezio will try to send the commands to a FIFO + instead of executing them locally. This is to be used in conjunction with + ssh and the daemon.py file. See the /link documentation for details. + +*remote_fifo_path*:: ./poezio.fifo + + The path of the FIFO used to send the commands (see the exec_remote option). + + Optional section options ~~~~~~~~~~~~~~~~~~~~~~~~ These option can appear in optional sections. These section are named @@ -420,8 +435,44 @@ foo = true *display_user_color_in_join_part*:: false + If set to true, the color of the nick will be used in MUCs information + messages, instead of the default color from the theme. + +*show_useless_separator*:: false + + If true, show the separator in a chat room, even if no one spoke. + *hide_exit_join*:: -1 + Exact same thing than hide_status_change, except that it concerns + the quit message, and that it will be hidden only if the value is 0. + Default setting means: + - all quit and join notices will be displayed + *hide_status_change*:: 120 + Set a number for this setting. + The join OR status-change notices will be + displayed according to this number. + -1: the notices will ALWAYS be displayed + 0: the notices will NEVER be displayed + n: On any other number, the notices will only be displayed + if the user involved has talked since the last n seconds + if the value is incorrect, -1 is assumed + Default setting means : + - status changes won't be displayed unless + the user talked in the last 2 minutes + *highlight_on*:: [empty] + + a list of words (separated by a colon (:)) that will be + highlighted if said by someone on a room + +*ignore_private*:: false + + Ignore private messages sent from this room. + +*private_auto_response*:: "Not in private, please." + + The message you want to be sent when someone tries to message you. + diff --git a/doc/en/index.txt b/doc/en/index.txt new file mode 100644 index 00000000..beb80d69 --- /dev/null +++ b/doc/en/index.txt @@ -0,0 +1,17 @@ +Poezio Documentation +==================== + +Welcome to the english documentation, here is a list of the availalble pages. + +Available pages +--------------- + + +* link:install.html[Installation] +* link:configure.html[Configuration] +* link:usage.html[Usage] +* link:themes.html[Theming] +* link:keys.html[Keys] +* link:plugins/index.html[Available Plugins] +* link:plugins.html[Developing plugins] +* link:xep.html[Current XEP support] diff --git a/doc/en/keys.txt b/doc/en/keys.txt index c1b9b7fd..874614df 100644 --- a/doc/en/keys.txt +++ b/doc/en/keys.txt @@ -95,6 +95,7 @@ height of the conversation window - 1. *Alt-v*:: Move the separator at the bottom of the tab. +*Alt-h*:: Scroll to the separator, if there is one. MultiUserChat tab input keys ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -105,6 +106,10 @@ These keys work only in the MultiUserChat tab. *Alt-y*:: Scroll the user list up. +*Alt-p*:: Scroll to the previous highlight. + +*Alt-n*:: Scroll to the next highlight. + *tabulation*:: Complete a nick. *Ctrl-c*:: Insert xhtml formatting. You have to press Ctrl-c then a character diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index 9d81ad66..3e8a0447 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -337,6 +337,12 @@ The handlers for this event are called when someone gets kicked in a MUC. * _presence_: Presence received. * _tab_: Tab of the concerned MUC. +*ignored_private*:: +message+ +tab+ + +The handlers for this event are called when a private message gets ignored. + +* _message_: Message received. +* _tab_: Tab of the concerned message. + SleekXMPP events ~~~~~~~~~~~~~~~~ diff --git a/doc/en/plugins/otr.txt b/doc/en/plugins/otr.txt index 26a6ed4e..533f6b4b 100644 --- a/doc/en/plugins/otr.txt +++ b/doc/en/plugins/otr.txt @@ -37,41 +37,45 @@ If not, then you will have to install it by hand. First, clone the repo and go inside the created directory: ============================================== +[source,bash] +------------- +git clone https://github.com/teisenbe/libopenotr.git - git clone https://git.teisen.be/repo/libopenotr.git - - cd libopenotr - +cd libopenotr +------------- ============================================== then run autogen.sh and configure ============ +[source,bash] +------------- +sh autogen.sh - sh autogen.sh - - ./configure --enable-gaping-security-hole - +./configure --enable-gaping-security-hole +------------- ============ Then compile & install the lib: ============ +[source,bash] +------------- +make - make - - sudo make install - +sudo make install +------------- ============ Finally, install the python module: ============================= +[source,bash] +------------- +python3 setup.py build - python3 setup.py build - - sudo python3 setup.py install - +sudo python3 setup.py install +------------- ============================= diff --git a/doc/en/plugins/simple_notify.txt b/doc/en/plugins/simple_notify.txt index f9ec0f74..c210f703 100644 --- a/doc/en/plugins/simple_notify.txt +++ b/doc/en/plugins/simple_notify.txt @@ -24,3 +24,7 @@ directly in the command line by the author of the message, and the body. The example shown above will display something like this: image:../../images/simple_notify_example.png["Simple notify example", title="Simple notify example"] + +NOTE: If you set the _exec_remote_ option to _true_ into the +link:../configure.html[main configuration file], the command will be executed +remotely (as explained in the link:link.html[/link help]). diff --git a/plugins/alias.py b/plugins/alias.py index d6a46b6f..5a35d1c6 100644 --- a/plugins/alias.py +++ b/plugins/alias.py @@ -1,3 +1,9 @@ +""" +Alias plugin. + +Allows the creation and the removal of personal aliases. +""" + from plugin import BasePlugin import common from common import parse_command_args_to_alias as parse @@ -9,14 +15,16 @@ class Plugin(BasePlugin): self.commands = {} def command_alias(self, line): + """ + /alias <alias> <command> [args] + """ arg = common.shell_split(line) if len(arg) < 2: self.core.information('Alias: Not enough parameters', 'Error') return alias = arg[0] - tmp_args = common.shell_split(arg[1]) - command = tmp_args.pop(0) - tmp_args = arg[1][len(command)+1:] + command = arg[1] + tmp_args = arg[2] if len(arg) > 2 else '' if alias in self.core.commands or alias in self.commands: self.core.information('Alias: command already exists', 'Error') @@ -26,6 +34,9 @@ class Plugin(BasePlugin): self.core.information('Alias /%s successfuly created' % alias, 'Info') def command_unalias(self, alias): + """ + /unalias <existing alias> + """ if alias in self.commands: del self.commands[alias] self.del_command(alias) diff --git a/plugins/link.py b/plugins/link.py index 2fcf9ddd..29ded32f 100644 --- a/plugins/link.py +++ b/plugins/link.py @@ -3,15 +3,18 @@ import re -from plugin import BasePlugin, PluginConfig +from plugin import BasePlugin from xhtml import clean_text import common +import tabs url_pattern = re.compile(r'\b(http[s]?://(?:\S+))\b', re.I|re.U) class Plugin(BasePlugin): def init(self): - self.add_command('link', self.command_link, "Usage: /link\nLink: opens the last link from the conversation into a browser.") + self.add_tab_command(tabs.MucTab, 'link', self.command_link, "Usage: /link\nLink: opens the last link from the conversation into a browser.") + self.add_tab_command(tabs.PrivateTab, 'link', self.command_link, "Usage: /link\nLink: opens the last link from the conversation into a browser.") + self.add_tab_command(tabs.ConversationTab, 'link', self.command_link, "Usage: /link\nLink: opens the last link from the conversation into a browser.") def find_link(self, nb): messages = self.core.get_conversation_messages() diff --git a/plugins/otr.py b/plugins/otr.py index b674c0fd..971b0059 100644 --- a/plugins/otr.py +++ b/plugins/otr.py @@ -16,7 +16,7 @@ class Plugin(BasePlugin): self.add_event_handler('conversation_say_after', self.on_conversation_say) self.add_event_handler('conversation_msg', self.on_conversation_msg) - self.add_command('otr', self.command_otr, "Usage: /otr <start|end>\notr: Start or stop OTR for the current conversation", self.otr_completion) + self.add_tab_command(ConversationTab, 'otr', self.command_otr, "Usage: /otr <start|end>\notr: Start or stop OTR for the current conversation", self.otr_completion) ConversationTab.add_information_element('otr', self.display_encryption_status) def cleanup(self): diff --git a/plugins/quote.py b/plugins/quote.py index 788d4027..50c390f2 100644 --- a/plugins/quote.py +++ b/plugins/quote.py @@ -1,7 +1,7 @@ -from plugin import BasePlugin, PluginConfig +from plugin import BasePlugin from xhtml import clean_text import common - +import tabs import re timestamp_re = re.compile(r'^(\d\d\d\d-\d\d-\d\d )?\d\d:\d\d:\d\d$') @@ -12,7 +12,9 @@ log = logging.getLogger(__name__) class Plugin(BasePlugin): def init(self): - self.add_command('quote', self.command_quote, "Usage: /quote <timestamp>\nQuote: takes the message received at <timestamp> and insert it in the input, to quote it.", self.completion_quote) + self.add_tab_command(tabs.MucTab, 'quote', self.command_quote, "Usage: /quote <timestamp>\nQuote: takes the message received at <timestamp> and insert it in the input, to quote it.", self.completion_quote) + self.add_tab_command(tabs.ConversationTab, 'quote', self.command_quote, "Usage: /quote <timestamp>\nQuote: takes the message received at <timestamp> and insert it in the input, to quote it.", self.completion_quote) + self.add_tab_command(tabs.PrivateTab, 'quote', self.command_quote, "Usage: /quote <timestamp>\nQuote: takes the message received at <timestamp> and insert it in the input, to quote it.", self.completion_quote) def command_quote(self, args): args = common.shell_split(args) diff --git a/src/core.py b/src/core.py index 9b704682..3ac0a0b5 100644 --- a/src/core.py +++ b/src/core.py @@ -246,6 +246,7 @@ class Core(object): self.xmpp.add_event_handler("groupchat_message", self.on_groupchat_message) self.xmpp.add_event_handler("groupchat_invite", self.on_groupchat_invite) self.xmpp.add_event_handler("groupchat_decline", self.on_groupchat_decline) + self.xmpp.add_event_handler("groupchat_config_status", self.on_status_codes) self.xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject) self.xmpp.add_event_handler("message", self.on_message) self.xmpp.add_event_handler("got_online" , self.on_got_online) @@ -657,12 +658,17 @@ class Core(object): self.add_tab(form_tab, True) def on_got_offline(self, presence): + """ + A JID got offline + """ jid = presence['from'] logger.log_roster_change(jid.bare, 'got offline') # If a resource got offline, display the message in the conversation with this # precise resource. if jid.resource: self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full)) + if jid.server in roster.blacklist: + return self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), 'Roster') if isinstance(self.current_tab(), tabs.RosterInfoTab): @@ -842,13 +848,19 @@ class Core(object): room_from = jid.bare body = xhtml.get_body_from_message_stanza(message) tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation + ignore = config.get_by_tabname('ignore_private', 'false', + room_from).lower() == 'true' if not tab: # It's the first message we receive: create the tab - if body: + if body and not ignore: tab = self.open_private_window(room_from, nick_from, False) - if not tab: - return + if ignore: + self.events.trigger('ignored_private', message, tab) + msg = config.get_by_tabname('private_auto_response', None, room_from) + if msg and body: + self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') + return self.events.trigger('private_msg', message, tab) - if not body: + if not body or not tab: return tab.add_message(body, time=None, nickname=nick_from, forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from)) @@ -979,6 +991,8 @@ class Core(object): """subscribed received""" jid = presence['from'].bare contact = roster[jid] + if contact.subscription not in ('both', 'from'): + self.information('%s accepted your contact proposal' % jid, 'Roster') if contact.pending_out: contact.pending_out = False if isinstance(self.current_tab(), tabs.RosterInfoTab): @@ -988,9 +1002,10 @@ class Core(object): """unsubscribe received""" jid = presence['from'].bare contact = roster[jid] - if contact.subscription in ('to', 'both'): - self.information('%s does not want to receive your status anymore.' % jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' + if not contact: + return + self.information('%s does not want to receive your status anymore.' % jid, 'Roster') + self.get_tab_by_number(0).state = 'highlight' if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -998,13 +1013,14 @@ class Core(object): """unsubscribed received""" jid = presence['from'].bare contact = roster[jid] - if contact.subscription in ('both', 'from'): - self.information('%s does not want you to receive his status anymore.'%jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - elif contact.pending_out: - self.information('%s rejected your contact proposal.' % jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' + if not contact: + return + if contact.pending_out: + self.information('%s rejected your contact proposal' % jid, 'Roster') contact.pending_out = False + else: + self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster') + self.get_tab_by_number(0).state = 'highlight' if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -1349,9 +1365,14 @@ class Core(object): if not subject or not tab: return if nick_from: - self.add_message_to_text_buffer(tab._text_buffer, _("%(nick)s set the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=None) + self.add_message_to_text_buffer(tab._text_buffer, + _("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % + {'info_col': get_theme().COLOR_INFORMATION_TEXT[0], 'nick':nick_from, 'subject':subject}, + time=None) else: - self.add_message_to_text_buffer(tab._text_buffer, _("The subject is: %(subject)s") % {'subject':subject}, time=None) + self.add_message_to_text_buffer(tab._text_buffer, _("\x19%(info_col)s}The subject is: %(subject)s") % + {'subject':subject, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}, + time=None) tab.topic = subject if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): self.refresh_window() @@ -1406,6 +1427,48 @@ class Core(object): if config.get_by_tabname('disable_beep', 'false', room_from, False).lower() != 'true': curses.beep() + def on_status_codes(self, message): + """ + Handle groupchat messages with status codes. + Those are received when a room configuration change occurs. + """ + room_from = message['from'] + tab = self.get_tab_by_name(room_from, tabs.MucTab) + status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))]) + if '101' in status_codes: + self.information('Your affiliation in the room %s changed' % room_from, 'Info') + elif tab and status_codes: + show_unavailable = '102' in status_codes + hide_unavailable = '103' in status_codes + non_priv = '104' in status_codes + logging_on = '170' in status_codes + logging_off= '171' in status_codes + non_anon = '172' in status_codes + semi_anon = '173' in status_codes + full_anon = '174' in status_codes + modif = False + if show_unavailable or hide_unavailable or non_priv or logging_off\ + or non_anon or semi_anon or full_anon: + tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + modif = True + if show_unavailable: + tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif hide_unavailable: + tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if non_anon: + tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif semi_anon: + tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif full_anon: + tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if logging_on: + tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif logging_off: + tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if modif: + self.refresh_window() + + def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None): """ Add the message to the room if possible, else, add it to the Info window @@ -1714,7 +1777,10 @@ class Core(object): if jid.resource or jid.full.endswith('/'): # we are writing the resource: complete the node if not the_input.last_completion: - response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) + try: + response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) + except: + response = None if response: items = response['disco_items'].get_items() else: @@ -1926,6 +1992,7 @@ class Core(object): else: b.method = "local" bookmark.save_local() + bookmark.save_remote(self.xmpp) self.information('Bookmarks added and saved.', 'Info') return else: @@ -1981,11 +2048,13 @@ class Core(object): if isinstance(tab, tabs.MucTab): b = bookmark.get_by_jid(tab.get_name()) if not b: - b = bookmark.Bookmark(tab.get_name(), autojoin=autojoin) + b = bookmark.Bookmark(tab.get_name(), autojoin=autojoin, + method=bookmark.preferred) bookmark.bookmarks.append(b) else: - b.method = "local" + b.method = bookmark.preferred if bookmark.save_remote(self.xmpp, self): + bookmark.save_local() self.information("Bookmarks added.", "Info") else: self.information("Could not add the bookmarks.", "Info") diff --git a/src/events.py b/src/events.py index 8def6cb0..e66c5ee5 100644 --- a/src/events.py +++ b/src/events.py @@ -40,6 +40,7 @@ class EventHandler(object): 'muc_nickchange': [], 'muc_ban': [], 'send_normal_presence': [], + 'ignored_private': [], } def add_event_handler(self, name, callback, position=0): @@ -63,7 +64,10 @@ class EventHandler(object): """ Call all the callbacks associated to the given event name. """ - callbacks = self.events[name] + callbacks = self.events.get(name, None) + if callbacks is None: + log.debug('%s: No such event.', name) + return for callback in callbacks: callback(*args, **kwargs) diff --git a/src/roster.py b/src/roster.py index 7f93c4b2..e1251024 100644 --- a/src/roster.py +++ b/src/roster.py @@ -19,6 +19,10 @@ from sleekxmpp.xmlstream.stanzabase import JID from sleekxmpp.exceptions import IqError class Roster(object): + + # MUC domains to blacklist from the contacts roster + blacklist = set() + def __init__(self): """ node: the RosterSingle from SleekXMPP @@ -103,7 +107,7 @@ class Roster(object): def jids(self): """List of the contact JIDS""" - return [key for key in self.__node.keys() if key not in self.__mucs and key != self.jid] + return [key for key in self.__node.keys() if JID(key).server not in self.blacklist and key != self.jid] def get_contacts(self): """ diff --git a/src/tabs.py b/src/tabs.py index 7bc51f6c..4e1c5141 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -8,9 +8,9 @@ """ a Tab object is a way to organize various Windows (see windows.py) around the screen at once. -A tab is then composed of multiple Buffer. +A tab is then composed of multiple Buffers. Each Tab object has different refresh() and resize() methods, defining how its -Windows are displayed, resized, etc +Windows are displayed, resized, etc. """ MIN_WIDTH = 42 @@ -216,11 +216,12 @@ class Tab(object): # one possibily. The next tab will complete the argument. # Otherwise we would need to add a useless space before being # able to complete the arguments. - hit_copy = the_input.hit_list[:] - for w in hit_copy[:]: - while hit_copy.count(w) > 1: - hit_copy.remove(w) - if len(hit_copy) in (1, 0): + hit_copy = set(the_input.hit_list) + while not hit_copy: + the_input.key_backspace() + the_input.auto_completion(words, '', quotify=False) + hit_copy = set(the_input.hit_list) + if len(hit_copy) == 1: the_input.do_command(' ') return True return False @@ -393,6 +394,7 @@ class ChatTab(Tab): # since the last input self.remote_supports_attention = False self.key_func['M-v'] = self.move_separator + self.key_func['M-h'] = self.scroll_separator self.key_func['M-/'] = self.last_words_completion self.key_func['^M'] = self.on_enter self.commands['say'] = (self.command_say, @@ -534,7 +536,7 @@ class ChatTab(Tab): def move_separator(self): self.text_win.remove_line_separator() - self.text_win.add_line_separator() + self.text_win.add_line_separator(self._text_buffer) self.text_win.refresh() self.input.refresh() @@ -565,6 +567,11 @@ class ChatTab(Tab): def on_half_scroll_down(self): self.text_win.scroll_down((self.text_win.height-1) // 2) + def scroll_separator(self): + self.text_win.scroll_to_separator() + self.refresh() + self.core.doupdate() + class MucTab(ChatTab): """ @@ -599,6 +606,8 @@ class MucTab(ChatTab): self.key_func['^I'] = self.completion self.key_func['M-u'] = self.scroll_user_list_down self.key_func['M-y'] = self.scroll_user_list_up + self.key_func['M-n'] = self.go_to_next_hl + self.key_func['M-p'] = self.go_to_prev_hl # commands self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname."), self.completion_ignore) self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."), self.completion_unignore) @@ -629,6 +638,22 @@ class MucTab(ChatTab): def general_jid(self): return self.get_name() + def go_to_next_hl(self): + """ + Go to the next HL in the room, or the last + """ + self.text_win.next_highlight() + self.refresh() + self.core.doupdate() + + def go_to_prev_hl(self): + """ + Go to the previous HL in the room, or the first + """ + self.text_win.previous_highlight() + self.refresh() + self.core.doupdate() + def completion_version(self, the_input): """Completion for /version""" compare_users = lambda x: x.last_talked @@ -808,6 +833,7 @@ class MucTab(ChatTab): /part [msg] """ arg = arg.strip() + msg = None if self.joined: self.disconnect() muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg) @@ -853,7 +879,8 @@ class MucTab(ChatTab): /topic [new topic] """ if not arg.strip(): - self._text_buffer.add_message(_("The subject of the room is: %s") % self.topic) + self._text_buffer.add_message(_("\x19%s}The subject of the room is: %s") % + (get_theme().COLOR_INFORMATION_TEXT[0], self.topic)) self.text_win.refresh() self.input.refresh() return @@ -1131,13 +1158,13 @@ class MucTab(ChatTab): else: self.state = 'disconnected' self.text_win.remove_line_separator() - self.text_win.add_line_separator() + self.text_win.add_line_separator(self._text_buffer) if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text(): self.send_chat_state('inactive') def on_gain_focus(self): self.state = 'current' - if self.text_win.built_lines and self.text_win.built_lines[-1] is None: + if self.text_win.built_lines and self.text_win.built_lines[-1] is None and config.getl('show_useless_separator', 'false') != 'true': self.text_win.remove_line_separator() curses.curs_set(1) if self.joined and config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text(): @@ -1172,6 +1199,7 @@ class MucTab(ChatTab): self.core.events.trigger('muc_join', presence, self) if from_nick == self.own_nick: self.joined = True + roster.blacklist.add(JID(from_room).server) if self.get_name() in self.core.initial_joins: self.core.initial_joins.remove(self.get_name()) self._state = 'normal' @@ -1181,8 +1209,12 @@ class MucTab(ChatTab): self.send_chat_state('active') new_user.color = get_theme().COLOR_OWN_NICK self.add_message(_("\x19%(info_col)s}Your nickname is \x193}%(nick)s") % {'nick': from_nick, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if '201' in status_codes: + self.add_message('\x19%(info_col)s}Info: The room has been created' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) if '170' in status_codes: self.add_message('\x191}Warning: \x19%(info_col)s}this room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if '100' in status_codes: + self.add_message('\x191}Warning: \x19%(info_col)s}This room is not anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) if self.core.current_tab() is not self: self.refresh_tab_win() self.core.current_tab().input.refresh() @@ -1192,6 +1224,8 @@ class MucTab(ChatTab): change_nick = '303' in status_codes kick = '307' in status_codes and typ == 'unavailable' ban = '301' in status_codes and typ == 'unavailable' + shutdown = '332' in status_codes and typ == 'unavailable' + non_member = '322' in status_codes and typ == 'unavailable' user = self.get_user_by_name(from_nick) # New user if not user: @@ -1210,6 +1244,12 @@ class MucTab(ChatTab): self.core.events.trigger('muc_kick', presence, self) self.core.on_user_left_private_conversation(from_room, from_nick, status) self.on_user_kicked(presence, user, from_nick) + elif shutdown: + self.core.events.trigger('muc_shutdown', presence, self) + self.on_muc_shutdown() + elif non_member: + self.core.events.trigger('muc_shutdown', presence, self) + self.on_non_member_kick() # user quit elif typ == 'unavailable': self.on_user_leave_groupchat(user, jid, status, from_nick, from_room) @@ -1223,6 +1263,16 @@ class MucTab(ChatTab): self.input.refresh() self.core.doupdate() + def on_non_member_kicked(self): + """We have been kicked because the MUC is members-only""" + self.add_message('\x19%(info_col)s}%You have been kicked because you are not a member and the room is now members-only.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + self.disconnect() + + def on_muc_shutdown(self): + """We have been kicked because the MUC service is shutting down""" + self.add_message('\x19%(info_col)s}%You have been kicked because the MUC service is shutting down.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + self.disconnect() + def on_user_join(self, from_nick, affiliation, show, status, role, jid): """ When a new user joins the groupchat @@ -1478,7 +1528,7 @@ class MucTab(ChatTab): if highlight: nick_color = highlight time = time or datetime.now() - self._text_buffer.add_message(txt, time, nickname, nick_color, history, user) + self._text_buffer.add_message(txt, time, nickname, nick_color, history, user, highlight=highlight) return highlight class PrivateTab(ChatTab): @@ -1518,7 +1568,26 @@ class PrivateTab(ChatTab): self.parent_muc.privates.remove(self) def completion(self): - self.complete_commands(self.input) + """ + Called when Tab is pressed, complete the nickname in the input + """ + if self.complete_commands(self.input): + return + + # If we are not completing a command or a command's argument, complete a nick + compare_users = lambda x: x.last_talked + word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\ + if user.nick != self.own_nick] + after = config.get('after_completion', ',')+" " + input_pos = self.input.pos + self.input.line_pos + if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\ + self.input.get_text()[:input_pos] == self.input.last_completion + after): + add_after = after + else: + add_after = '' + self.input.auto_completion(word_list, add_after, quotify=False) + 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_after) def command_say(self, line, attention=False): if not self.on: @@ -1644,7 +1713,7 @@ class PrivateTab(ChatTab): def on_lose_focus(self): self.state = 'normal' self.text_win.remove_line_separator() - self.text_win.add_line_separator() + self.text_win.add_line_separator(self._text_buffer) tab = self.core.get_tab_by_name(JID(self.name).bare, MucTab) if tab and tab.joined and config.get_by_tabname( 'send_chat_states', 'true', self.general_jid, True) == 'true'\ @@ -2562,7 +2631,7 @@ class ConversationTab(ChatTab): resource = None self.state = 'normal' self.text_win.remove_line_separator() - self.text_win.add_line_separator() + self.text_win.add_line_separator(self._text_buffer) if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and (not self.input.get_text() or not self.input.get_text().startswith('//')): if resource: self.send_chat_state('inactive') diff --git a/src/text_buffer.py b/src/text_buffer.py index 9b717882..b615e96c 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -35,7 +35,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, history=None, user=None): + def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None, user=None, highlight=False): time = time or datetime.now() if txt.startswith('/me '): if nick_color: @@ -45,7 +45,7 @@ class TextBuffer(object): else: color = None # TODO: display the bg color too. - txt = ("\x19%(info_col)s}* \x19%(col)s}" % {'col':color or 5, 'info_col':get_theme().COLOR_INFORMATION_TEXT[0]})+ nickname + ' \x19%(info_col)s}' % {'info_col':get_theme().COLOR_INFORMATION_TEXT[0]} + txt[4:] + txt = '\x19%(info_col)s}* \x19%(col)s}%(nick)s \x19%(info_col)s}%(msg)s' % {'info_col':get_theme().COLOR_ME_MESSAGE[0], 'col': color or 5, 'nick': nickname, 'msg': txt[4:]} nickname = None msg = Message(txt='%s\x19o'%(txt.replace('\t', ' '),), nick_color=nick_color, time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\ @@ -57,7 +57,7 @@ class TextBuffer(object): ret_val = None for window in self.windows: # make the associated windows # build the lines from the new message - nb = window.build_new_message(msg, history=history) + nb = window.build_new_message(msg, history=history, highlight=highlight) if ret_val is None: ret_val = nb if window.pos != 0: diff --git a/src/theming.py b/src/theming.py index e45a25ff..94d7b005 100644 --- a/src/theming.py +++ b/src/theming.py @@ -108,6 +108,9 @@ class Theme(object): CHAR_AFFILIATION_MEMBER = '+' CHAR_AFFILIATION_NONE = '-' + # Color for the /me message + COLOR_ME_MESSAGE = (6, -1) + # Separators COLOR_VERTICAL_SEPARATOR = (4, -1) COLOR_NEW_TEXT_SEPARATOR = (2, -1) diff --git a/src/windows.py b/src/windows.py index 7185346e..06214abb 100644 --- a/src/windows.py +++ b/src/windows.py @@ -51,7 +51,7 @@ def truncate_nick(nick, size=None): size = size or config.get('max_nick_length', 25) if size < 1: size = 1 - if nick and len(nick) >= size: + if nick and len(nick) > size: return nick[:size]+'…' return nick @@ -620,9 +620,17 @@ class TextWin(Win): self.pos = 0 self.built_lines = [] # Each new message is built and kept here. # on resize, we rebuild all the messages + self.lock = False self.lock_buffer = [] + # the Lines of the highlights in that buffer + self.highlights = [] + # the current HL position in that list + self.hl_pos = -1 + + self.separator_after = None + def toggle_lock(self): if self.lock: self.release_lock() @@ -637,6 +645,74 @@ class TextWin(Win): self.built_lines.append(line) self.lock = False + def next_highlight(self): + """ + Go to the next highlight in the buffer. + (depending on which highlight was selected before) + if the buffer is already positionned on the last, of if there are no + highlights, scroll to the end of the buffer. + """ + log.debug('Going to the next highlight…') + if not self.highlights or self.hl_pos == -1 or \ + self.hl_pos == len(self.highlights)-1: + self.hl_pos = -1 + self.pos = 0 + return + hl_size = len(self.highlights) - 1 + if self.hl_pos < hl_size: + self.hl_pos += 1 + else: + self.hl_pos = hl_size + + hl = self.highlights[self.hl_pos] + pos = None + while not pos: + try: + pos = self.built_lines.index(hl) + except ValueError: + self.highlights = self.highlights[self.hl_pos+1:] + if not self.highlights: + self.hl_pos = -1 + self.pos = 0 + return + hl = self.highlights[0] + self.pos = len(self.built_lines) - pos - self.height + if self.pos < 0 or self.pos >= len(self.built_lines): + self.pos = 0 + + def previous_highlight(self): + """ + Go to the previous highlight in the buffer. + (depending on which highlight was selected before) + if the buffer is already positionned on the first, or if there are no + highlights, scroll to the end of the buffer. + """ + log.debug('Going to the previous highlight…') + if not self.highlights or self.hl_pos == 0: + self.hl_pos = -1 + self.pos = 0 + return + if self.hl_pos < 0: + self.hl_pos = len(self.highlights) - 1 + elif self.hl_pos > 0: + self.hl_pos -= 1 + + hl = self.highlights[self.hl_pos] + pos = None + while not pos: + try: + pos = self.built_lines.index(hl) + except ValueError: + self.highlights = self.highlights[self.hl_pos+1:] + if not self.highlights: + self.hl_pos = -1 + self.pos = 0 + return + hl = self.highlights[0] + self.pos = len(self.built_lines) - pos - self.height + if self.pos < 0 or self.pos >= len(self.built_lines): + self.pos = 0 + def scroll_up(self, dist=14): self.pos += dist if self.pos + self.height > len(self.built_lines): @@ -655,11 +731,11 @@ class TextWin(Win): present, scroll at the top of the window """ if None in self.built_lines: - self.pos = self.built_lines.index(None) + self.pos = len(self.built_lines) - self.built_lines.index(None) - self.height + 1 + if self.pos < 0: + self.pos = 0 # Chose a proper position (not too high) self.scroll_up(0) - else: # Go at the top of the win - self.pos = len(self.built_lines) - self.height def remove_line_separator(self): """ @@ -668,15 +744,20 @@ class TextWin(Win): log.debug('remove_line_separator') if None in self.built_lines: self.built_lines.remove(None) + self.separator_after = None - def add_line_separator(self): + def add_line_separator(self, room=None): """ add a line separator at the end of messages list + room is a textbuffer that is needed to get the previous message + (in case of resize) """ if None not in self.built_lines: self.built_lines.append(None) + if room and room.messages: + self.separator_after = room.messages[-1] - def build_new_message(self, message, history=None, clean=True): + def build_new_message(self, message, history=None, clean=True, highlight=False): """ Take one message, build it and add it to the list Return the number of lines that are built for the given @@ -703,10 +784,13 @@ class TextWin(Win): start_pos=line[0], end_pos=line[1])) else: + for line in lines: - self.built_lines.append(Line(msg=message, - start_pos=line[0], - end_pos=line[1])) + saved_line = Line(msg=message,start_pos=line[0],end_pos=line[1]) + self.built_lines.append(saved_line) + if highlight: + highlight = False + self.highlights.append(saved_line) if clean: while len(self.built_lines) > self.lines_nb_limit: self.built_lines.pop(0) @@ -789,6 +873,8 @@ class TextWin(Win): self.built_lines = [] for message in room.messages: self.build_new_message(message, clean=False) + if self.separator_after is message: + self.build_new_message(None) while len(self.built_lines) > self.lines_nb_limit: self.built_lines.pop(0) diff --git a/src/xhtml.py b/src/xhtml.py index cf7a5fc0..38ec690c 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -206,7 +206,10 @@ def ncurses_color_to_html(color): html color. """ if color <= 15: - (r, g, b) = curses.color_content(color) + try: + (r, g, b) = curses.color_content(color) + except: # fallback in faulty terminals (e.g. xterm) + (r, g, b) = curses.color_content(color%8) r = r / 1000 * 6 - 0.01 g = g / 1000 * 6 - 0.01 b = b / 1000 * 6 - 0.01 |