summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/default_config.cfg19
-rw-r--r--doc/en/configure.txt51
-rw-r--r--doc/en/index.txt17
-rw-r--r--doc/en/keys.txt5
-rw-r--r--doc/en/plugins.txt6
-rw-r--r--doc/en/plugins/otr.txt36
-rw-r--r--doc/en/plugins/simple_notify.txt4
-rw-r--r--plugins/alias.py17
-rw-r--r--plugins/link.py7
-rw-r--r--plugins/otr.py2
-rw-r--r--plugins/quote.py8
-rw-r--r--src/core.py105
-rw-r--r--src/events.py6
-rw-r--r--src/roster.py6
-rw-r--r--src/tabs.py99
-rw-r--r--src/text_buffer.py6
-rw-r--r--src/theming.py3
-rw-r--r--src/windows.py104
-rw-r--r--src/xhtml.py5
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