diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | data/default_config.cfg | 13 | ||||
-rw-r--r-- | data/themes/dark.py | 2 | ||||
-rw-r--r-- | doc/en/configure.txt | 5 | ||||
-rw-r--r-- | doc/en/plugins/gpg.txt | 2 | ||||
-rw-r--r-- | doc/en/plugins/simple_notify.txt | 26 | ||||
-rw-r--r-- | doc/en/usage.txt | 38 | ||||
-rw-r--r-- | doc/images/simple_notify_example.png | bin | 0 -> 6623 bytes | |||
-rw-r--r-- | plugins/alias.py | 2 | ||||
-rw-r--r-- | plugins/amsg.py | 13 | ||||
-rw-r--r-- | plugins/gpg/__init__.py | 3 | ||||
-rw-r--r-- | plugins/ping.py | 2 | ||||
-rw-r--r-- | plugins/simple_notify.py | 34 | ||||
-rw-r--r-- | src/bookmark.py | 2 | ||||
-rw-r--r-- | src/common.py | 11 | ||||
-rw-r--r-- | src/config.py | 27 | ||||
-rw-r--r-- | src/connection.py | 5 | ||||
-rw-r--r-- | src/core.py | 180 | ||||
-rwxr-xr-x | src/daemon.py | 5 | ||||
-rw-r--r-- | src/data_forms.py | 24 | ||||
-rw-r--r-- | src/multiuserchat.py | 18 | ||||
-rw-r--r-- | src/tabs.py | 208 | ||||
-rw-r--r-- | src/text_buffer.py | 3 | ||||
-rw-r--r-- | src/theming.py | 34 | ||||
-rw-r--r-- | src/windows.py | 68 | ||||
-rw-r--r-- | src/xhtml.py | 8 |
26 files changed, 488 insertions, 249 deletions
@@ -17,11 +17,13 @@ clean: install: all mkdir -p $(DESTDIR)$(prefix) - install -d $(DESTDIR)$(LOCALEDIR) $(DESTDIR)$(BINDIR) $(DESTDIR)$(DATADIR)/poezio $(DESTDIR)$(DATADIR)/poezio/data $(DESTDIR)$(DATADIR)/poezio/src/ $(DESTDIR)$(DATADIR)/poezio/src $(DESTDIR)$(DATADIR)/poezio/data/themes $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(DOCDIR)/poezio + install -d $(DESTDIR)$(LOCALEDIR) $(DESTDIR)$(BINDIR) $(DESTDIR)$(DATADIR)/poezio $(DESTDIR)$(DATADIR)/poezio/data $(DESTDIR)$(DATADIR)/poezio/src/ $(DESTDIR)$(DATADIR)/poezio/src $(DESTDIR)$(DATADIR)/poezio/data/themes $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(DOCDIR)/poezio $(DESTDIR)$(DATADIR)/poezio/plugins cp -R data/* $(DESTDIR)$(DATADIR)/poezio/data/ rm $(DESTDIR)$(DATADIR)/poezio/data/poezio.1 + cp -R plugins/* $(DESTDIR)$(DATADIR)/poezio/plugins + cp -R doc/* $(DESTDIR)$(DOCDIR)/poezio/ cp README CHANGELOG COPYING $(DESTDIR)$(DOCDIR)/poezio/ diff --git a/data/default_config.cfg b/data/default_config.cfg index e870941d..4188851b 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -10,6 +10,10 @@ server = anon.louiz.org # the port you'll use to connect port = 5222 +# Auto-reconnects you when you get disconnected from the server +# defaults to false because it should not be necessary +auto_reconnect = false + # the resource you will use # If it's empty, your resource will be chosen (most likely randomly) by the server # It is not recommended to use a resource that is easy to guess, because it can lead @@ -174,7 +178,8 @@ show_inactive_tabs = true # - 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 +# - invite (when you receive an invitation for joining a MUC) +beep_on = highlight private invite # Theme @@ -197,6 +202,12 @@ vertical_tab_list_size = 20 vertical_tab_list_sort = desc +# Show the user list at the bottom when in a MUC +# (useful when you want to look at the bottom of the screen only) +# 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 diff --git a/data/themes/dark.py b/data/themes/dark.py index 272c4d91..c7942290 100644 --- a/data/themes/dark.py +++ b/data/themes/dark.py @@ -20,6 +20,7 @@ class DarkTheme(theming.Theme): COLOR_TAB_CURRENT = (-1, 16) COLOR_TAB_NEW_MESSAGE = (3, 236) COLOR_TAB_HIGHLIGHT = (1, 236) + COLOR_TAB_ATTENTION = (6, 236) COLOR_TAB_PRIVATE = (2, 236) COLOR_TAB_DISCONNECTED = (13, 236) @@ -36,6 +37,7 @@ class DarkTheme(theming.Theme): COLOR_VERTICAL_TAB_NEW_MESSAGE = (3, -1) COLOR_VERTICAL_TAB_HIGHLIGHT = (1, -1) COLOR_VERTICAL_TAB_PRIVATE = (2, -1) + COLOR_VERTICAL_TAB_ATTENTION = (6, -1) COLOR_VERTICAL_TAB_DISCONNECTED = (13, -1) diff --git a/doc/en/configure.txt b/doc/en/configure.txt index 4e93a2c5..1fca2e36 100644 --- a/doc/en/configure.txt +++ b/doc/en/configure.txt @@ -55,6 +55,11 @@ section of this documentation. It is not recommended to use a resource that is easy to guess, because it can lead to presence leak. +*auto_reconnect*:: false + + Auto-reconnects you when you get disconnected. Should not be necessary, so + the default is false. + *default_nick*:: [empty] diff --git a/doc/en/plugins/gpg.txt b/doc/en/plugins/gpg.txt index 70a6fd15..a39b68a4 100644 --- a/doc/en/plugins/gpg.txt +++ b/doc/en/plugins/gpg.txt @@ -33,7 +33,7 @@ You need to create a plugin configuration file. Create a file named _gpg.cfg_ into your plugins configuration directory (_~/.config/poezio/plugins_ by default), and fill it like this: -[source,python] +[source,conf] --------------------------------------------------------------------- [gpg] keyid = 091F9C78 diff --git a/doc/en/plugins/simple_notify.txt b/doc/en/plugins/simple_notify.txt new file mode 100644 index 00000000..f9ec0f74 --- /dev/null +++ b/doc/en/plugins/simple_notify.txt @@ -0,0 +1,26 @@ +Simple Notify +============= + +This plugin lets you execute a command, to notify you from new important +messages. + +Installation and configuration +------------------------------ + +You need to create a plugin configuration file. Create a file named _simple_notify.cfg_ +into your plugins configuration directory (_~/.config/poezio/plugins_ by +default), and fill it like this: + +[source,conf] +--------------------------------------------------------------------- +[simple_notify] +command = notify-send -i /path/to/poezio/data/poezio_80.png "New message from %(from)s" "%(body)s" +--------------------------------------------------------------------- + +You can put any command, instead of this one. You can also use the +special keywords _%(from)s_ and _%(body)s_ that will be replaced +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"] diff --git a/doc/en/usage.txt b/doc/en/usage.txt index 5f220fd4..4db1d699 100644 --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -14,7 +14,7 @@ On all tabs, you get a line showing the the list of all opened tabs. Each tab has a number, each time you open a new tab, it gets the next available number. -image::../images/tab_bar.png[title="Example of 5 opened tabs"] +image:../images/tab_bar.png[title="Example of 5 opened tabs"] The tab numbered _0_ is always the _roster_ tab, the other tabs can be of any type. @@ -51,7 +51,7 @@ a conversation with one of them. Use the _arrows_ to browse the list, the _space_ key to fold or unfold a group or a contact. -image::../images/roster.png["The roster tab", title="The roster tab"] +image:../images/roster.png["The roster tab", title="The roster tab"] * _1_: The area where information messages are displayed. * _2_: The actual list of contacts. The first level is group, the second is the @@ -63,7 +63,7 @@ MultiUserChat tab This tab contains a multi-users conversation. -image::../images/muc.png["The MUC tab", title="The MUC tab"] +image:../images/muc.png["The MUC tab", title="The MUC tab"] * _1_: The conversation window, this is where all the messages and events related to the muc will be displayed. It can be scrolled up and down with @@ -95,7 +95,7 @@ Private tab This is the tab opened with the _/query_ command, letting you talk in private with a participant of a multi-users chat. -image::../images/private.png["The private tab", title="The private tab"] +image:../images/private.png["The private tab", title="The private tab"] This is just a simple one to one conversation, with a line showing the status, name and chatstate of the participant. @@ -104,7 +104,7 @@ Conversation tab ~~~~~~~~~~~~~~~~ A tab opened from the roster, to talk in private with one of your contacts. -image::../images/conversation.png["The conversation tab", title="The conversation tab"] +image:../images/conversation.png["The conversation tab", title="The conversation tab"] This is also just a simple one to one conversation, with a line showing the status, name and chatstate of the participant, as well as a line at the top showing the @@ -117,7 +117,7 @@ This tab lets you view a form receive from a remote entity, edit the values and send everything back. It is mostly used to configure MUCs with the _/configure_ command but can actually be used for almost anything. -image::../images/data_forms.png["The dataform tab", title="The dataform tab"] +image:../images/data_forms.png["The dataform tab", title="The dataform tab"] Use the _up_ and _down_ keys to go from one field to the other, and edit the value using the _space_, _left_ or _right_ keys, or by entering text. @@ -131,7 +131,7 @@ This tab lists all public rooms on a MUC service. It is currently very limited but will be improved in the future. There currently is no way to search a room or even to sort them. -image::../images/list.png["The list tab", title="The list tab"] +image:../images/list.png["The list tab", title="The list tab"] Use the _up_ and _down_ or _PageUp_ and _PageDown_ keys to browse the list, and use _Enter_ or _j_ to join the selected room. @@ -220,13 +220,14 @@ These commands work in *any* tab. */bookmarks*:: Show the current bookmarks. -*/set <option> [value]*:: Set the value to the option in your configuration - file. You can, for example, change your default nickname by doing "/set - default_nick toto" or your resource with "/set resource blabla". You can also - set an empty value (nothing) by providing no [value] after <option>. - -*/set_plugin <plugin> <option> <value>*:: Set the value of the option in a - plugin configuration file. +*/set [plugin|][section] <option> <value>*:: Set the value to the option in your + configuration file. You can, for example, change your default nickname by + doing "/set default_nick toto" or your resource with "/set resource blabla". + Doing so will write in the main config file, and in the main section + ([Poezio]). But you can also write to another section, with "/set bindings + M-i ^i", to a plugin configuration with "/set mpd_client| host main" (notice + the *|*, it is mandatory to write in a plugin), or even to another section + in a plugin configuration "/set plugin|other_section option value". */theme [theme_name]*:: Reload the theme defined in the config file. If _theme_name_ is given, this command will act like /set theme theme_name then @@ -247,6 +248,13 @@ These commands work in *any* tab. */version <jid>*:: Get the software version of the given JID (usually its XMPP client and Operating System). +*/invite <jid> <room> [reason]*:: Invite _jid_ to _room_ wit _reason_ (if + provided). + +*/decline <room> [reason]*:: Decline invitation to _room_ with _reason_. + +*/invitations*:: Show the pending invitations. + */server_cycle [server.tld] [message]*:: Disconnect and reconnect in all the rooms of server.tld. @@ -296,6 +304,8 @@ MultiUserChat tab commands close the tab. You can specify an optional message if you are still connected. */nick <nickname>*:: Change your nickname in the current room. + *Except for gmail users* because gmail.com sucks and will do weird things + if you change your nickname in a MUC. */recolor [random]*:: Re-assign a color to all the participants in the current room, based on the last time they talked. Use this if the participants diff --git a/doc/images/simple_notify_example.png b/doc/images/simple_notify_example.png Binary files differnew file mode 100644 index 00000000..e9a54399 --- /dev/null +++ b/doc/images/simple_notify_example.png diff --git a/plugins/alias.py b/plugins/alias.py index 2517e2b9..d6a46b6f 100644 --- a/plugins/alias.py +++ b/plugins/alias.py @@ -21,7 +21,7 @@ class Plugin(BasePlugin): if alias in self.core.commands or alias in self.commands: self.core.information('Alias: command already exists', 'Error') return - self.commands[alias] = lambda args: self.get_command(command)(parse(common.shell_split(args), tmp_args)) + self.commands[alias] = lambda arg: self.get_command(command)(parse(arg, tmp_args)) self.add_command(alias, self.commands[alias], 'This command is an alias for /%s %s' %( command, tmp_args)) self.core.information('Alias /%s successfuly created' % alias, 'Info') diff --git a/plugins/amsg.py b/plugins/amsg.py new file mode 100644 index 00000000..697f793f --- /dev/null +++ b/plugins/amsg.py @@ -0,0 +1,13 @@ +# A simple broadcast plugin + +from plugin import BasePlugin +from tabs import MucTab + +class Plugin(BasePlugin): + def init(self): + self.add_command('amsg', self.command_amsg, "Usage: /amsg <message>\nAmsg: Broadcast the message to all the joined rooms.") + + def command_amsg(self, args): + for room in self.core.tabs: + if isinstance(room, MucTab) and room.joined: + room.command_say(args) diff --git a/plugins/gpg/__init__.py b/plugins/gpg/__init__.py index 01ca6ab2..00d896cd 100644 --- a/plugins/gpg/__init__.py +++ b/plugins/gpg/__init__.py @@ -158,7 +158,6 @@ class Plugin(BasePlugin): if jid.full not in self.contacts.keys(): return '' status = self.contacts[jid.full] - self.core.information('%s' % (status,)) if status in ('valid', 'invalid', 'signed'): return ' GPG Key: %s (%s)' % (status, 'encrypted' if status == 'valid' else 'NOT encrypted',) else: @@ -176,6 +175,8 @@ class Plugin(BasePlugin): else: if isinstance(self.core.current_tab(), ConversationTab): jid = JID(self.core.current_tab().get_name()) + else: + return command = args[0] if command == 'force' or command == 'enable': # we can force encryption only with contact having an associated diff --git a/plugins/ping.py b/plugins/ping.py index 349b7f52..51198d39 100644 --- a/plugins/ping.py +++ b/plugins/ping.py @@ -18,7 +18,7 @@ class Plugin(BasePlugin): return jid = JID(arg) try: - delay = self.core.xmpp.plugin['xep_0199'].send_ping(jid=jid) + delay = self.core.xmpp.plugin['xep_0199'].send_ping(jid=jid, block=False) except: delay = None if delay is not None: diff --git a/plugins/simple_notify.py b/plugins/simple_notify.py new file mode 100644 index 00000000..32861d87 --- /dev/null +++ b/plugins/simple_notify.py @@ -0,0 +1,34 @@ +# A plugin that adds the /link command, letting you open links that are pasted +# in the conversation, without having to click them. + +import os +import re + +from plugin import BasePlugin, PluginConfig +from xhtml import clean_text, get_body_from_message_stanza +import common + +url_pattern = re.compile(r'\b(http[s]?://(?:\S+))\b', re.I|re.U) + +class Plugin(BasePlugin): + def init(self): + self.add_event_handler('private_msg', self.on_private_msg) + self.add_event_handler('conversation_msg', self.on_conversation_msg) + + def on_private_msg(self, message, tab): + fro = message['from'] + self.do_notify(message, fro) + + def on_conversation_msg(self, message, tab): + fro = message['from'].bare + self.do_notify(message, fro) + + def do_notify(self, message, fro): + body = clean_text(get_body_from_message_stanza(message)) + if not body: + return + command = self.config.get('command', '').strip() + if not command: + self.core.information('No notification command was provided in the configuration file', 'Warning') + return + self.core.exec_command(command % {'body':body, 'from':fro}) diff --git a/src/bookmark.py b/src/bookmark.py index 45616d93..b63ddbdf 100644 --- a/src/bookmark.py +++ b/src/bookmark.py @@ -78,7 +78,7 @@ class Bookmark(object): """ jid = el.get('jid') name = el.get('name') - autojoin = True if el.get('autojoin', False) == 'true' else False + autojoin = True if el.get('autojoin', 'false').lower() in ('true', '1') else False nick = None for n in iter(el, 'nick'): nick = nick.text diff --git a/src/common.py b/src/common.py index 2da7835b..26b5dd0f 100644 --- a/src/common.py +++ b/src/common.py @@ -235,13 +235,16 @@ def parse_secs_to_str(duration=0): result += '%ss' % secs if secs else '' return result -def parse_command_args_to_alias(args, strto): +def parse_command_args_to_alias(arg, strto): """ Parse command parameters. Numbers can be from 0 to 9. - >>> parse_command_args_to_alias(['sdf', 'koin'], '%0 %1') - "sdf koin" + >>> parse_command_args_to_alias('sdf koin', '%1 %0') + "koin sdf" """ + if '%' not in strto: + return strto + arg + args = shell_split(arg) l = len(args) dest = '' var_num = False @@ -250,7 +253,7 @@ def parse_command_args_to_alias(args, strto): if not var_num: dest += i elif i in string.digits: - if int(i) < l: + if 0 <= int(i) < l: dest += args[int(i)] var_num = False elif i == '%': diff --git a/src/config.py b/src/config.py index af9c9fbe..2ee4abb1 100644 --- a/src/config.py +++ b/src/config.py @@ -113,16 +113,31 @@ class Config(RawConfigParser): df.close() result_lines = [] we_are_in_the_right_section = False + written = False + section_found = False for line in lines_before: if line.startswith('['): # check the section + if we_are_in_the_right_section and not written: + result_lines.append('%s= %s' % (option, value)) if line == '[%s]' % section: we_are_in_the_right_section = True + section_found = True else: we_are_in_the_right_section = False if (line.startswith('%s ' % (option,)) or - line.startswith('%s=' % (option,))) and we_are_in_the_right_section: - line = '%s = %s' % (option, value) + line.startswith('%s=' % (option,)) or + line.startswith('%s = ' % (option,))) and we_are_in_the_right_section: + line = '%s= %s' % (option, value) + written = True result_lines.append(line) + + if not section_found: + result_lines.append('[%s]' % section) + result_lines.append('%s= %s' % (option, value)) + elif not written: + result_lines.append('%s= %s' % (option, value)) + + df = open(self.file_name, 'w') for line in result_lines: df.write('%s\n' % line) @@ -133,11 +148,11 @@ class Config(RawConfigParser): set the value in the configuration then save it to the file """ - try: + if self.has_section(section): + RawConfigParser.set(self, section, option, value) + else: + self.add_section(section) RawConfigParser.set(self, section, option, value) - except NoSectionError: - # TODO, add this section if it didn't exist - return self.write_in_file(section, option, value) def set(self, option, value, section=DEFSECTION): diff --git a/src/connection.py b/src/connection.py index 352a1d5b..8a7c1ea7 100644 --- a/src/connection.py +++ b/src/connection.py @@ -40,9 +40,9 @@ class Connection(sleekxmpp.ClientXMPP): self.anon = True jid = '%s/%s' % (config.get('server', 'anon.louiz.org'), resource) password = None - sleekxmpp.ClientXMPP.__init__(self, jid, password, ssl=True) + sleekxmpp.ClientXMPP.__init__(self, jid, password) self.core = None - self.auto_reconnect = False + self.auto_reconnect = True if config.get('auto_reconnect', 'false').lower() in ('true', '1') else False self.auto_authorize = None self.register_plugin('xep_0030') self.register_plugin('xep_0004') @@ -59,6 +59,7 @@ class Connection(sleekxmpp.ClientXMPP): self.register_plugin('xep_0092', pconfig=info) if config.get('send_time', 'true') == 'true': self.register_plugin('xep_0202') + self.register_plugin('xep_0224') def start(self): # TODO, try multiple servers diff --git a/src/core.py b/src/core.py index 3bb0436e..dfea7dd3 100644 --- a/src/core.py +++ b/src/core.py @@ -60,7 +60,7 @@ from daemon import Executor ERROR_AND_STATUS_CODES = { '401': _('A password is required'), '403': _('Permission denied'), - '404': _('The room does\'nt exist'), + '404': _('The room doesn’t exist'), '405': _('Your are not allowed to create a new room'), '406': _('A reserved nick must be used'), '407': _('You are not in the member list'), @@ -128,7 +128,7 @@ class Core(object): 'status': (self.command_status, _('Usage: /status <availability> [status message]\nStatus: Sets your availability and (optionally) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status message] argument will be your status message.'), self.completion_status), 'bookmark_local': (self.command_bookmark_local, _("Usage: /bookmark_local [roomname][/nick]\nBookmark Local: Bookmark locally the specified room (you will then auto-join it on each poezio start). This commands uses almost the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), self.completion_bookmark_local), 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick] [autojoin] [password]\nBookmark: Bookmark online the specified room (you will then auto-join it on each poezio start if autojoin is specified and is 'true'). This commands uses almost the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), self.completion_bookmark), - 'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Set the value of the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>."), self.completion_set), + 'set': (self.command_set, _("Usage: /set [plugin|][section] <option> [value]\nSet: Set the value of an option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set options in specific sections with `/set bindings M-i ^i` or in specific plugin with `/set mpd_client| host 127.0.0.1`"), None), 'theme': (self.command_theme, _('Usage: /theme [theme_name]\nTheme: Reload the theme defined in the config file. If theme_name is provided, set that theme before reloading it.'), self.completion_theme), 'list': (self.command_list, _('Usage: /list\nList: Get the list of public chatrooms on the specified server.'), self.completion_list), 'message': (self.command_message, _('Usage: /message <jid> [optional message]\nMessage: Open a conversation with the specified JID (even if it is not in our roster), and send a message to it, if the message is specified.'), self.completion_version), @@ -141,9 +141,9 @@ class Core(object): 'plugins': (self.command_plugins, _('Usage: /plugins\nPlugins: Show the plugins in use.'), None), 'presence': (self.command_presence, _('Usage: /presence <JID> [type] [status]\nPresence: Send a directed presence to <JID> and using [type] and [status] if provided.'), self.completion_presence), 'rawxml': (self.command_rawxml, _('Usage: /rawxml\nRawXML: Send a custom xml stanza.'), None), - 'set_plugin': (self.command_set_plugin, _("Usage: /set_plugin <plugin> <option> [value]\nSet Plugin: Set the value of the option in a plugin configuration file."), self.completion_set_plugin), 'invite': (self.command_invite, _("Usage: /invite <jid> <room> [reason]\nInvite: Invite jid in room with reason."), self.completion_invite), 'decline': (self.command_decline, _("Usage: /decline <room> [reason]\nDecline: Decline the invitation to room with or without reason."), self.completion_decline), + 'invitations': (self.command_invitations, _("Usage: /invites\nInvites: Show the pending invitations."), None), 'bookmarks': (self.command_bookmarks, _("Usage: /bookmarks\nBookmarks: Show the current bookmarks."), None), 'remove_bookmark': (self.command_remove_bookmark, _("Usage: /remove_bookmark [jid]\nRemove Bookmark: Remove the specified bookmark, or the bookmark on the current tab, if any."), self.completion_remove_bookmark), 'xml_tab': (self.command_xml_tab, _("Usage: /xml_tab\nXML Tab: Open an XML tab."), None), @@ -193,6 +193,7 @@ class Core(object): self.xmpp.add_event_handler("chatstate_paused", self.on_chatstate_paused) self.xmpp.add_event_handler("chatstate_gone", self.on_chatstate_gone) self.xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive) + self.xmpp.add_event_handler("attention", self.on_attention) self.xmpp.register_handler(Callback('ALL THE STANZAS', connection.MatchAll(None), self.incoming_stanza)) self.timed_events = set() @@ -371,9 +372,12 @@ class Core(object): if password: msg += ". The password is \"%s\"." % password self.information(msg, 'Info') + if 'invite' in config.get('beep_on', 'invite').split(): + curses.beep() self.pending_invites[jid.bare] = inviter.full def command_invite(self, arg): + """/invite <to> <room> [reason]""" args = common.shell_split(arg) if len(args) < 2: return @@ -383,14 +387,15 @@ class Core(object): self.xmpp.plugin['xep_0045'].invite(room, to, reason) def completion_invite(self, the_input): + """Completion for /invite""" txt = the_input.get_text() args = common.shell_split(txt) n = len(args) if txt.endswith(' '): n += 1 - if len(args) == 1: + if n == 2: return the_input.auto_completion([contact.bare_jid for contact in roster.get_contacts()], '') - elif len(args) == 2: + elif n == 3: rooms = [] for tab in self.tabs: if isinstance(tab, tabs.MucTab) and tab.joined: @@ -398,6 +403,7 @@ class Core(object): return the_input.auto_completion(rooms, '') def command_decline(self, arg): + """/decline <room@server.tld> [reason]""" args = common.shell_split(arg) if not len(args): return @@ -405,17 +411,30 @@ class Core(object): if jid.bare not in self.pending_invites: return reason = args[1] if len(args) > 1 else '' + del self.pending_invites[jid.bare] self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason) def completion_decline(self, the_input): + """Completion for /decline""" txt = the_input.get_text() args = common.shell_split(txt) n = len(args) if txt.endswith(' '): n += 1 - if len(args) == 1: + if n == 2: return the_input.auto_completion(list(self.pending_invites.keys()), '') + def command_invitations(self, arg): + """/invitations""" + build = "" + for invite in self.pending_invites: + build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare) + if self.pending_invites: + build = "You are invited to the following rooms:\n" + build + else: + build = "You are do not have any pending invitation." + self.information(build, 'Info') + def on_groupchat_decline(self, decline): pass @@ -484,6 +503,21 @@ class Core(object): tab.input.refresh() self.doupdate() + def on_attention(self, message): + jid_from = message['from'] + self.information('%s requests your attention!' % jid_from, 'Info') + for tab in self.tabs: + if tab.get_name() == jid_from: + tab.state = 'attention' + self.refresh_tab_win() + return + for tab in self.tabs: + if tab.get_name() == jid_from.bare: + tab.state = 'attention' + self.refresh_tab_win() + return + self.information('%s tab not found.' % jid_from, 'Error') + def open_new_form(self, form, on_cancel, on_send, **kwargs): """ Open a new tab containing the form @@ -749,20 +783,25 @@ class Core(object): """ jid = message['from'] body = xhtml.get_body_from_message_stanza(message) - conversation = self.get_tab_of_conversation_with_jid(jid, create=False) 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) self.events.trigger('conversation_msg', message, conversation) - body = xhtml.get_body_from_message_stanza(message) if roster.get_contact_by_jid(jid.bare): remote_nick = roster.get_contact_by_jid(jid.bare).name or jid.user else: remote_nick = jid.user - conversation._text_buffer.add_message(body, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER) - if conversation.remote_wants_chatstates is None: + delay_tag = message.find('{urn:xmpp:delay}delay') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + delayed = False + date = None + conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed) + if conversation.remote_wants_chatstates is None and not delayed: if message['chat_state']: conversation.remote_wants_chatstates = True else: @@ -1046,31 +1085,16 @@ class Core(object): def go_to_important_room(self): """ - Go to the next room with activity, in this order: - - A personal conversation with a new message - - A Muc with an highlight - - A Muc with any new message + Go to the next room with activity, in the order defined in the + dict tabs.STATE_PRIORITY """ - for tab in self.tabs: - if tab.state == 'private': - self.command_win('%s' % tab.nb) - return - for tab in self.tabs: - if tab.state == 'highlight': - self.command_win('%s' % tab.nb) - return - for tab in self.tabs: - if tab.state == 'message': - self.command_win('%s' % tab.nb) - return - for tab in self.tabs: - if tab.state == 'disconnected': - self.command_win('%s' % tab.nb) - return - for tab in self.tabs: - if isinstance(tab, tabs.ChatTab) and not tab.input.is_empty(): - self.command_win('%s' % tab.nb) - return + priority = tabs.STATE_PRIORITY + sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state], + reverse=True) + tab = sorted_tabs.pop(0) if sorted_tabs else None + if priority[tab.state] < 0 or not tab: + return + self.command_win('%s' % tab.nb) def rotate_rooms_right(self, args=None): """ @@ -1827,18 +1851,32 @@ class Core(object): def command_set(self, arg): """ - /set <option> [value] + /set [module|][section] <option> <value> """ - args = arg.split() - if len(args) != 2 and len(args) != 1: + args = common.shell_split(arg) + if len(args) != 2 and len(args) != 3: self.command_help('set') return - option = args[0] if len(args) == 2: + option = args[0] value = args[1] - else: - value = '' - config.set_and_save(option, value) + config.set_and_save(option, value) + elif len(args) == 3: + if '|' in args[0]: + plugin_name, section = args[0].split('|') + if not section: + section = plugin_name + option = args[1] + value = args[2] + if not plugin_name in self.plugin_manager.plugins: + return + plugin = self.plugin_manager.plugins[plugin_name] + plugin.config.set_and_save(option, value, section) + else: + section = args[0] + option = args[1] + value = args[2] + config.set_and_save(option, value, section) msg = "%s=%s" % (option, value) self.information(msg, 'Info') @@ -1858,61 +1896,6 @@ class Core(object): serv_list.append(serv) return the_input.auto_completion(serv_list, ' ') - def completion_set(self, the_input): - """Completion for /set""" - txt = the_input.get_text() - args = txt.split() - n = len(args) - if txt.endswith(' '): - n += 1 - if n == 2: - return the_input.auto_completion(config.options('Poezio'), '') - elif n == 3: - return the_input.auto_completion([config.get(args[1], '')], '') - - def command_set_plugin(self, arg): - """ - /set_plugin <plugin> <option> [value] - """ - args = arg.split() - if len(args) != 3 and len(args) != 2: - self.command_help('set_plugin') - return - plugin_name = args[0] - if not plugin_name in self.plugin_manager.plugins: - return - plugin = self.plugin_manager.plugins[plugin_name] - option = args[1] - if len(args) == 3: - value = args[2] - else: - value = '' - plugin.config.set_and_save(option, value, plugin_name) - if not plugin.config.write(): - self.core.information('Could not save the plugin config', 'Error') - return - msg = "%s=%s" % (option, value) - self.information(msg, 'Info') - - def completion_set_plugin(self, the_input): - """Completion for /set_plugin""" - txt = the_input.get_text() - args = txt.split() - n = len(args) - if txt.endswith(' '): - n += 1 - - if n == 2: - return the_input.auto_completion(list(self.plugin_manager.plugins.keys()), '') - elif n == 3: - if not args[1] in self.plugin_manager.plugins: - return - return the_input.auto_completion(self.plugin_manager.plugins[args[1]].config.options(args[1]), '') - elif n == 4: - if not args[1] in self.plugin_manager.plugins: - return - return the_input.auto_completion([self.plugin_manager.plugins[args[1]].config.get(args[2], '', args[1])], ' ') - def close_tab(self, tab=None): """ Close the given tab. If None, close the current one @@ -2145,7 +2128,10 @@ class Core(object): self.remote_fifo = None else: e = Executor(command.strip()) - e.start() + try: + e.start() + except ValueError as e: # whenever shlex fails + self.information('%s' % (e,), 'Error') def get_conversation_messages(self): """ diff --git a/src/daemon.py b/src/daemon.py index b413f465..4b4c0b79 100755 --- a/src/daemon.py +++ b/src/daemon.py @@ -22,7 +22,7 @@ command on your local machine. import sys import threading import subprocess - +import shlex import logging log = logging.getLogger(__name__) @@ -40,7 +40,8 @@ class Executor(threading.Thread): def run(self): log.info('executing %s' % (self.command.strip(),)) - subprocess.call(self.command.split()) + command = shlex.split(self.command) + subprocess.call(command) def main(): while True: diff --git a/src/data_forms.py b/src/data_forms.py index 2d17a304..d38f392a 100644 --- a/src/data_forms.py +++ b/src/data_forms.py @@ -67,15 +67,16 @@ class DataFormsTab(Tab): def resize(self): self.need_resize = False self.topic_win.resize(1, self.width, 0, 0) - self.tab_win.resize(1, self.width, self.height-2, 0) - self.form_win.resize(self.height-4, self.width, 1, 0) + self.form_win.resize(self.height-3 - Tab.tab_win_height(), self.width, 1, 0) self.help_win.resize(1, self.width, self.height-1, 0) - self.help_win_dyn.resize(1, self.width, self.height-3, 0) + self.help_win_dyn.resize(1, self.width, self.height-2 - Tab.tab_win_height(), 0) self.lines = [] def refresh(self): + if self.need_resize: + self.resize() self.topic_win.refresh(self._form['title']) - self.tab_win.refresh() + self.refresh_tab_win() self.help_win.refresh() self.help_win_dyn.refresh(self.form_win.get_help_message()) self.form_win.refresh() @@ -88,7 +89,7 @@ class FieldInput(object): """ def __init__(self, field): self._field = field - self.color = (14, -1) + self.color = get_theme().COLOR_NORMAL_TEXT def set_color(self, color): self.color = color @@ -131,6 +132,7 @@ class ColoredLabel(windows.Win): def refresh(self): with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addstr(0, 0, self.text) self._win.attroff(to_curses_attr(self.color)) @@ -157,7 +159,7 @@ class DummyInput(FieldInput, windows.Win): class ColoredLabel(windows.Win): def __init__(self, text): self.text = text - self.color = (14, -1) + self.color = get_theme().COLOR_NORMAL_TEXT windows.Win.__init__(self) def resize(self, height, width, y, x): @@ -169,6 +171,7 @@ class ColoredLabel(windows.Win): def refresh(self): with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addstr(0, 0, self.text) self._win.attroff(to_curses_attr(self.color)) @@ -189,6 +192,7 @@ class BooleanWin(FieldInput, windows.Win): def refresh(self): with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addnstr(0, 0, ' '*(8), self.width) self.addstr(0, 2, "%s"%self.value) @@ -253,6 +257,7 @@ class TextMultiWin(FieldInput, windows.Win): def refresh(self): if not self.edition_input: with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addnstr(0, 0, ' '*self.width, self.width) option = self.options[self.val_pos] @@ -305,6 +310,7 @@ class ListMultiWin(FieldInput, windows.Win): def refresh(self): with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addnstr(0, 0, ' '*self.width, self.width) if self.val_pos > 0: @@ -351,6 +357,7 @@ class ListSingleWin(FieldInput, windows.Win): def refresh(self): with g_lock: + self._win.erase() self._win.attron(to_curses_attr(self.color)) self.addnstr(0, 0, ' '*self.width, self.width) if self.val_pos > 0: @@ -377,7 +384,7 @@ class TextSingleWin(FieldInput, windows.Input): self.text = field.getValue() if isinstance(field.getValue(), str)\ else "" self.pos = len(self.text) - self.color = (14, -1) + self.color = get_theme().COLOR_NORMAL_TEXT def reply(self): self._field['label'] = '' @@ -427,7 +434,7 @@ class FormWin(object): } def __init__(self, form, height, width, y, x): self._form = form - self._win = curses.newwin(height, width, y, x) + self._win = windows.Win._tab_win.derwin(height, width, y, x) self.current_input = 0 self.inputs = [] # dict list for (name, field) in self._form.getFields().items(): @@ -447,7 +454,6 @@ class FormWin(object): 'input':inp}) def resize(self, height, width, y, x): - self._win.resize(height, width) self.height = height self.width = width if self.current_input >= self.height-2: diff --git a/src/multiuserchat.py b/src/multiuserchat.py index f537c2c1..3f0c80b8 100644 --- a/src/multiuserchat.py +++ b/src/multiuserchat.py @@ -88,21 +88,11 @@ def set_user_role(xmpp, jid, nick, reason, role): except Exception as e: return e.iq -def set_user_affiliation(xmpp, jid, nick, reason, affiliation): +def set_user_affiliation(xmpp, muc_jid, affiliation, nick=None, jid=None, reason=None): """ (try to) Set the affiliation of a MUC user """ - iq = xmpp.makeIqSet() - query = ET.Element('{%s}query' % NS_MUC_ADMIN) - item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'affiliation':affiliation}) - if reason: - reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN) - reason_el.text = reason - item.append(reason_el) - query.append(item) - iq.append(query) - iq['to'] = jid try: - return iq.send() - except Exception as e: - return e.iq + return xmpp.plugin['xep_0045'].set_affiliation(muc_jid, jid, nick, affiliation) + except: + return False diff --git a/src/tabs.py b/src/tabs.py index e696f46d..c98bbed4 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -62,33 +62,36 @@ NS_MUC_USER = 'http://jabber.org/protocol/muc#user' STATE_COLORS = { 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED, + 'joined': lambda: get_theme().COLOR_TAB_JOINED, 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE, 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT, 'private': lambda: get_theme().COLOR_TAB_PRIVATE, 'normal': lambda: get_theme().COLOR_TAB_NORMAL, 'current': lambda: get_theme().COLOR_TAB_CURRENT, -# 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, + 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, } VERTICAL_STATE_COLORS = { 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED, + 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED, 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE, 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT, 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE, 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL, 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT, -# 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, + 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION, } STATE_PRIORITY = { 'normal': -1, 'current': -1, - 'disconnected': 0, 'message': 1, + 'joined': 1, 'highlight': 2, 'private': 2, -# 'attention': 3 + 'disconnected': 3, + 'attention': 3 } class Tab(object): @@ -155,8 +158,8 @@ class Tab(object): if not value in STATE_COLORS: log.debug("Invalid value for tab state: %s", value) elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \ - value != 'current': - log.debug("Did not set status because of lower priority, asked: %s, kept: %s", (value, self.state)) + value != 'current' and value != 'joined': + log.debug("Did not set status because of lower priority, asked: %s, kept: %s", value, self._state) else: self._state = value @@ -349,6 +352,7 @@ class ChatTab(Tab): # if that’s None, then no paused chatstate was sent recently # if that’s a weakref returning None, then a paused chatstate was sent # since the last input + self.remote_supports_attention = False self.key_func['M-v'] = self.move_separator self.key_func['M-/'] = self.last_words_completion self.key_func['^M'] = self.on_enter @@ -495,6 +499,7 @@ class ChatTab(Tab): def command_say(self, line): raise NotImplementedError + class MucTab(ChatTab): """ The tab containing a multi-user-chat room. @@ -531,7 +536,7 @@ class MucTab(ChatTab): self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."), self.completion_unignore) self.commands['kick'] = (self.command_kick, _("Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason."), self.completion_ignore) self.commands['role'] = (self.command_role, _("Usage: /role <nick> <role> [reason]\nRole: Set the role of an user. Roles can be: none, visitor, participant, moderator. You also can give an optional reason."), self.completion_role) - self.commands['affiliation'] = (self.command_affiliation, _("Usage: /affiliation <nick> <affiliation> [reason]\nAffiliation: Set the affiliation of an user. Affiliations can be: none, member, admin, owner. You also can give an optional reason."), self.completion_affiliation) + self.commands['affiliation'] = (self.command_affiliation, _("Usage: /affiliation <nick or jid> <affiliation>\nAffiliation: Set the affiliation of an user. Affiliations can be: outcast, none, member, admin, owner."), self.completion_affiliation) self.commands['topic'] = (self.command_topic, _("Usage: /topic <subject>\nTopic: Change the subject of the room."), self.completion_topic) self.commands['query'] = (self.command_query, _('Usage: /query <nick> [message]\nQuery: Open a private conversation with <nick>. This nick has to be present in the room you\'re currently in. If you specified a message after the nickname, it will immediately be sent to this user.'), self.completion_ignore) self.commands['part'] = (self.command_part, _("Usage: /part [message]\nPart: Disconnect from a room. You can specify an optional message."), None) @@ -543,6 +548,10 @@ class MucTab(ChatTab): self.commands['configure'] = (self.command_configure, _('Usage: /configure\nConfigure: Configure the current room, through a form.'), None) self.commands['version'] = (self.command_version, _('Usage: /version <jid or nick>\nVersion: Get the software version of the given JID or nick in room (usually its XMPP client and Operating System).'), self.completion_version) self.commands['names'] = (self.command_names, _('Usage: /names\nNames: Get the list of the users in the room, and the list of the people assuming the different roles.'), None) + + if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks + del self.commands["nick"] + self.resize() self.update_commands() self.update_keys() @@ -600,6 +609,10 @@ class MucTab(ChatTab): n += 1 if n == 2: userlist = [user.nick for user in self.users] + userlist.remove(self.own_nick) + jidlist = [user.jid.bare for user in self.users] + jidlist.remove(self.core.xmpp.boundjid.bare) + userlist.extend(jidlist) return the_input.auto_completion(userlist, '') elif n == 3: possible_affiliations = ['none', 'member', 'admin', 'owner'] @@ -678,6 +691,7 @@ class MucTab(ChatTab): for i, user in enumerate(sorted_users): user.color = colors[i % len(colors)] self.text_win.rebuild_everything(self._text_buffer) + self.user_win.refresh(self.users) self.text_win.refresh() self.input.refresh() @@ -758,7 +772,7 @@ class MucTab(ChatTab): if user.nick == nick: r = self.core.open_private_window(self.name, user.nick) if r and len(args) > 1: - msg = arg[len(nick)+1:] + msg = args[1] self.core.current_tab().command_say(xhtml.convert_simple_to_full_colors(msg)) if not r: self.core.information(_("Cannot find user: %s" % nick), 'Error') @@ -787,25 +801,36 @@ class MucTab(ChatTab): color_participant = get_theme().COLOR_USER_PARTICIPANT[0] color_information = get_theme().COLOR_INFORMATION_TEXT[0] visitors, moderators, participants, others = [], [], [], [] + aff = { + 'owner': lambda: get_theme().CHAR_AFFILIATION_OWNER, + 'admin': lambda: get_theme().CHAR_AFFILIATION_ADMIN, + 'member': lambda: get_theme().CHAR_AFFILIATION_MEMBER, + 'none': lambda: get_theme().CHAR_AFFILIATION_NONE, + } + for user in self.users: if user.role == 'visitor': - visitors.append(user.nick) + visitors.append((user.nick, aff[user.affiliation]())) elif user.role == 'participant': - participants.append(user.nick) + participants.append((user.nick, aff[user.affiliation]())) elif user.role == 'moderator': - moderators.append(user.nick) + moderators.append((user.nick, aff[user.affiliation]())) else: - others.append(user.nick) + others.append((user.nick, aff[user.affiliation]())) message = 'Users: %s \n' % len(self.users) for moderator in moderators: - message += ' \x19%s}%s\x19o -' % (color_moderator, moderator) + message += ' [%s] \x19%s}%s\x19o -' % (moderator[1], + color_moderator, moderator[0]) for participant in participants: - message += ' \x19%s}%s\x19o -' % (color_participant, participant) + message += ' [%s] \x19%s}%s\x19o -' % (participant[1], + color_participant, participant[0]) for visitor in visitors: - message += ' \x19%s}%s\x19o -' % (color_visitor, visitor) + message += ' [%s] \x19%s}%s\x19o -' % (visitor[1], + color_visitor, visitor[0]) for other in others: - message += ' \x19%s}%s\x19o -' % (color_other, other) + message += ' [%s] \x19%s}%s\x19o -' % (other[1], + color_other, other[0]) message = message[:-2] self._text_buffer.add_message(message) @@ -814,7 +839,7 @@ class MucTab(ChatTab): def completion_topic(self, the_input): current_topic = self.topic - return the_input.auto_completion([current_topic], '') + return the_input.auto_completion([current_topic], '', quotify=False) def command_kick(self, arg): """ @@ -854,7 +879,7 @@ class MucTab(ChatTab): def command_affiliation(self, arg): """ - /affiliation <nick> <role> [reason] + /affiliation <nick> <role> Changes the affiliation of an user roles can be: none, visitor, participant, moderator """ @@ -867,14 +892,14 @@ class MucTab(ChatTab): reason = ' '.join(args[2:]) else: reason = '' - if not self.joined or \ - not affiliation in ('none', 'member', 'admin', 'owner'): -# replace this ↑ with this ↓ when the ban list support is done -# not affiliation in ('outcast', 'none', 'member', 'admin', 'owner'): + if not self.joined: return - res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), nick, reason, affiliation) - if res['type'] == 'error': - self.core.room_error(res, self.get_name()) + if nick in [user.nick for user in self.users]: + res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, nick=nick) + else: + res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, jid=nick) + if not res: + self.core.information('Could not set affiliation', 'Error') def command_say(self, line): needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active' @@ -1055,12 +1080,14 @@ class MucTab(ChatTab): self.users.append(new_user) if from_nick == self.own_nick: self.joined = True + if self != self.core.current_tab(): + self.state = 'joined' if self.core.current_tab() == self and self.core.status.show not in ('xa', 'away'): self.send_chat_state('active') new_user.color = get_theme().COLOR_OWN_NICK - self.add_message(_("\x195}Your nickname is \x193}%s") % (from_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 '170' in status_codes: - self.add_message('\x191}Warning: \x195}this room is publicly logged') + self.add_message('\x191}Warning: \x19%(info_col)s}this room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) else: change_nick = '303' in status_codes kick = '307' in status_codes and typ == 'unavailable' @@ -1089,6 +1116,9 @@ class MucTab(ChatTab): self.info_header.refresh(self, self.text_win) self.input.refresh() self.core.doupdate() + else: + self.core.current_tab().refresh_tab_win() + self.core.doupdate() def on_user_join(self, from_nick, affiliation, show, status, role, jid): """ @@ -1101,9 +1131,9 @@ class MucTab(ChatTab): if hide_exit_join != 0: color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 if not jid.full: - self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN}) + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} joined the room' % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) else: - self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s \x195}(\x194}%(jid)s\x195}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full}) + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s \x19%(info_col)s}(\x194}%(jid)s\x19%(info_col)s}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) self.core.on_user_rejoined_private_conversation(self.name, from_nick) def on_user_nick_change(self, presence, user, from_nick, from_room): @@ -1116,7 +1146,7 @@ class MucTab(ChatTab): _tab.own_nick = new_nick user.change_nick(new_nick) color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 - self.add_message('\x19%(color)d}%(old)s\x195} is now known as \x19%(color)d}%(new)s' % {'old':from_nick, 'new':new_nick, 'color':color}) + self.add_message('\x19%(color)d}%(old)s\x19%(info_col)s} is now known as \x19%(color)d}%(new)s' % {'old':from_nick, 'new':new_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) # rename the private tabs if needed self.core.rename_private_tabs(self.name, from_nick, new_nick) @@ -1134,17 +1164,17 @@ class MucTab(ChatTab): self.refresh_tab_win() self.core.doupdate() if by: - kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} + kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned by \x194}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned.') % {'spec':get_theme().CHAR_KICK} + kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned.') % {'spec':get_theme().CHAR_KICK, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 if by: - kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} if reason is not None and reason.text: - kick_msg += _('\x195} Reason: \x196}%(reason)s\x195}') % {'reason': reason.text} + kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s\x19%(info_col)s}') % {'reason': reason.text, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} self._text_buffer.add_message(kick_msg) def on_user_kicked(self, presence, user, from_nick): @@ -1161,20 +1191,20 @@ class MucTab(ChatTab): self.refresh_tab_win() self.core.doupdate() if by: - kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by} + kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked.') % {'spec':get_theme().CHAR_KICK} + kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked.') % {'spec':get_theme().CHAR_KICK, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} # try to auto-rejoin if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true': muc.join_groupchat(self.core.xmpp, self.name, self.own_nick) else: color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 if by: - kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'color':color, 'by':by.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} if reason is not None and reason.text: - kick_msg += _('\x195} Reason: \x196}%(reason)s') % {'reason': reason.text} + kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s') % {'reason': reason.text, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} self.add_message(kick_msg) def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room): @@ -1192,9 +1222,9 @@ class MucTab(ChatTab): if hide_exit_join == -1 or user.has_talked_since(hide_exit_join): color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 if not jid.full: - leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT} + leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} (\x194}%(jid)s\x195}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full} + leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} (\x194}%(jid)s\x19%(info_col)s}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} if status: leave_msg += ' (%s)' % status self.add_message(leave_msg) @@ -1210,9 +1240,9 @@ class MucTab(ChatTab): # to be displayed has changed color = user.color[0] if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3 if from_nick == self.own_nick: - msg = _('\x193}You\x195} changed: ') + msg = _('\x193}You\x19%(info_col)s} changed: ') % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: - msg = _('\x19%(color)d}%(nick)s\x195} changed: ') % {'nick': from_nick.replace('"', '\\"'), 'color': color} + msg = _('\x19%(color)d}%(nick)s\x19%(info_col)s} changed: ') % {'nick': from_nick, 'color': color, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} if show not in SHOW_NAME: self.core.information("%s from room %s sent an invalid show: %s" %\ (from_nick, from_room, show), "warning") @@ -1318,6 +1348,7 @@ class MucTab(ChatTab): 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 in the room anymore + Return True if the message highlighted us. False otherwise. """ self.log_message(txt, time, nickname) user = self.get_user_by_name(nickname) if nickname is not None else None @@ -1331,14 +1362,16 @@ class MucTab(ChatTab): if self.state != 'highlight': self.state = 'message' nick_color = nick_color or None + highlight = False if (not nickname or time) and not txt.startswith('/me '): - txt = '\x195}%s' % (txt,) + txt = '\x19%(info_col)s}%(txt)s' % {'txt':txt, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]} else: # TODO highlight = self.do_highlight(txt, time, nickname) if highlight: nick_color = highlight time = time or datetime.now() self._text_buffer.add_message(txt, time, nickname, nick_color, history, user) + return highlight class PrivateTab(ChatTab): """ @@ -1355,6 +1388,7 @@ class PrivateTab(ChatTab): self._text_buffer.add_window(self.text_win) self.info_header = windows.PrivateInfoWin() self.input = windows.MessageInput() + self.check_attention() # keys self.key_func['^I'] = self.completion # commands @@ -1375,12 +1409,13 @@ class PrivateTab(ChatTab): def completion(self): self.complete_commands(self.input) - def command_say(self, line): + def command_say(self, line, attention=False): if not self.on: return msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' msg['body'] = line + logger.log_message(self.get_name().replace('/', '\\'), self.own_nick, line) # trigger the event BEFORE looking for colors. # This lets a plugin insert \x19xxx} colors, that will # be converted in xhtml. @@ -1392,12 +1427,34 @@ class PrivateTab(ChatTab): if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False: needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active' msg['chat_state'] = needed + if attention and self.remote_supports_attention: + msg['attention'] = True self.core.events.trigger('private_say_after', msg, self) msg.send() self.cancel_paused_delay() self.text_win.refresh() self.input.refresh() + def command_attention(self, message=''): + if message is not '': + self.command_say(message, attention=True) + else: + msg = self.core.xmpp.make_message(self.get_name()) + msg['type'] = 'chat' + msg['attention'] = True + msg.send() + + def check_attention(self): + self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked) + + def on_attention_checked(self, iq): + if 'urn:xmpp:attention:0' in iq['disco_info'].get_features(): + self.core.information('Attention is supported', 'Info') + self.remote_supports_attention = True + self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None) + else: + self.remote_supports_attention = False + def command_unquery(self, arg): """ /unquery @@ -1416,6 +1473,8 @@ class PrivateTab(ChatTab): res.get('version') or _('unknown'), res.get('os') or _('on an unknown platform')) self.core.information(version, 'Info') + if arg: + return self.core.command_version(arg) jid = self.name self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) @@ -1503,7 +1562,7 @@ class PrivateTab(ChatTab): The user changed her nick in the corresponding muc: update the tab’s name and display a message. """ - self.add_message('\x193}%(old)s\x195} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick}) + self.add_message('\x193}%(old)s\x19%(info_col)s} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) new_jid = JID(self.name).bare+'/'+new_nick self.name = new_jid @@ -1513,9 +1572,9 @@ class PrivateTab(ChatTab): """ self.deactivate() if not status_message: - self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT.replace('"', '\\"')}) + self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) else: - self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) + self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) if self.core.current_tab() is self: self.refresh() self.core.doupdate() @@ -1531,7 +1590,7 @@ class PrivateTab(ChatTab): user = tab.get_user_by_name(nick) if user: color = user.color[0] - self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN}) + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x19%(info_col)s} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) if self.core.current_tab() is self: self.refresh() self.core.doupdate() @@ -1663,7 +1722,7 @@ class RosterInfoTab(Tab): roster.remove_contact(jid) except Exception as e: import traceback - log.debug(_('Traceback when removing %s from the roster:\n')+traceback.format_exc()) + log.debug(_('Traceback when removing %s from the roster:\n' % jid)+traceback.format_exc()) def command_add(self, args): """ @@ -2090,6 +2149,7 @@ class ConversationTab(ChatTab): self.upper_bar = windows.ConversationStatusMessageWin() self.info_header = windows.ConversationInfoWin() self.input = windows.MessageInput() + self.check_attention() # keys self.key_func['^I'] = self.completion # commands @@ -2119,7 +2179,7 @@ class ConversationTab(ChatTab): def completion(self): self.complete_commands(self.input) - def command_say(self, line): + def command_say(self, line, attention=False): msg = self.core.xmpp.make_message(self.get_name()) msg['type'] = 'chat' msg['body'] = line @@ -2135,6 +2195,8 @@ class ConversationTab(ChatTab): if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False: needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active' msg['chat_state'] = needed + if attention and self.remote_supports_attention: + msg['attention'] = True self.core.events.trigger('conversation_say_after', msg, self) msg.send() logger.log_message(JID(self.get_name()).bare, self.core.own_nick, line) @@ -2150,10 +2212,30 @@ class ConversationTab(ChatTab): else: resource = contact.get_highest_priority_resource() if resource: - self._text_buffer.add_message("\x195}Status: %s\x193}" %resource.status, None, None, None, None, None) + self._text_buffer.add_message("\x19%(info_col)s}Status: %(status)s\x193}" % {'status': resource.status, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}, None, None, None, None, None) self.refresh() self.core.doupdate() + def command_attention(self, message=''): + if message is not '': + self.command_say(message, attention=True) + else: + msg = self.core.xmpp.make_message(self.get_name()) + msg['type'] = 'chat' + msg['attention'] = True + msg.send() + + def check_attention(self): + self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked) + + def on_attention_checked(self, iq): + if 'urn:xmpp:attention:0' in iq['disco_info'].get_features(): + self.core.information('Attention is supported', 'Info') + self.remote_supports_attention = True + self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None) + else: + self.remote_supports_attention = False + def command_unquery(self, arg): self.core.close_tab() @@ -2169,6 +2251,8 @@ class ConversationTab(ChatTab): res.get('version') or _('unknown'), res.get('os') or _('on an unknown platform')) self.core.information(version, 'Info') + if arg: + return self.core.command_version(arg) jid = self._name self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) @@ -2363,7 +2447,23 @@ class MucListTab(Tab): 'name': item[2] or '' ,'users': ''} for item in iq['disco_items'].get_items()] self.listview.add_lines(items) self.upper_message.set_message('Chatroom list on server %s' % self.name) - self.upper_message.refresh() + if self.core.current_tab() is self: + self.listview.refresh() + self.upper_message.refresh() + else: + self.state = 'highlight' + self.refresh_tab_win() + curses.doupdate() + + def sort_by(self): + if self.list_header.get_order(): + self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=False) + self.list_header.set_order(False) + self.list_header.refresh() + else: + self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=True) + self.list_header.set_order(True) + self.list_header.refresh() curses.doupdate() def sort_by(self): diff --git a/src/text_buffer.py b/src/text_buffer.py index 3541b9c1..9b717882 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -16,6 +16,7 @@ import collections from datetime import datetime from config import config +from theming import get_theme Message = collections.namedtuple('Message', 'txt nick_color time str_time nickname user') @@ -44,7 +45,7 @@ class TextBuffer(object): else: color = None # TODO: display the bg color too. - txt = ("\x19%s}* \x195}" % (color or 5,))+ nickname + ' ' + txt[4:] + 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:] 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")\ diff --git a/src/theming.py b/src/theming.py index 7e90a5a7..c29d044d 100644 --- a/src/theming.py +++ b/src/theming.py @@ -11,16 +11,17 @@ used when drawing the interface. Colors are numbers from -1 to 7 (if only 8 colors are supported) or -1 to 255 if 256 colors are available. -We check the number of available colors at startup, and we load a theme accordingly. -A 8 color theme should NEVER use colors not in the -1 -> 7 range. We won't check that -at run time. If the case occurs, the THEME should be fixed. +If only 8 colors are available, all colors > 8 are converted using the +table_256_to_16 dict. + XHTML-IM colors are converted to -1 -> 255 colors if available, or directly to -1 -> 8 if we are in 8-color-mode. A pair_color is a background-foreground pair. All possible pairs are not created at startup, because that would create 256*256 pairs, and almost all of them would never be used. -So, a theme should define color tuples, like (200, -1), and when they are to + +A theme should define color tuples, like (200, -1), and when they are to be used by poezio's interface, they will be created once, and kept in a list for later usage. A color tuple is of the form (foreground, background, optional) @@ -78,7 +79,7 @@ class Theme(object): """ # Message text color COLOR_NORMAL_TEXT = (-1, -1) - COLOR_INFORMATION_TEXT = (137, -1) # TODO + COLOR_INFORMATION_TEXT = (5, -1) # TODO COLOR_HIGHLIGHT_NICK = (3, 5, 'b') # User list color @@ -100,6 +101,13 @@ class Theme(object): CHAR_CHATSTATE_COMPOSING = 'X' CHAR_CHATSTATE_PAUSED = 'p' + # These characters are used for the affiliation in the user list + # in a MUC + CHAR_AFFILIATION_OWNER = '~' + CHAR_AFFILIATION_ADMIN = '&' + CHAR_AFFILIATION_MEMBER = '+' + CHAR_AFFILIATION_NONE = '-' + # Separators COLOR_VERTICAL_SEPARATOR = (4, -1) COLOR_NEW_TEXT_SEPARATOR = (2, -1) @@ -114,17 +122,21 @@ class Theme(object): # Tabs COLOR_TAB_NORMAL = (7, 4) + COLOR_TAB_JOINED = (82, 4) COLOR_TAB_CURRENT = (7, 6) COLOR_TAB_NEW_MESSAGE = (7, 5) - COLOR_TAB_HIGHLIGHT = (7, 1) + COLOR_TAB_HIGHLIGHT = (7, 3) COLOR_TAB_PRIVATE = (7, 2) + COLOR_TAB_ATTENTION = (7, 1) COLOR_TAB_DISCONNECTED = (7, 8) COLOR_VERTICAL_TAB_NORMAL = (4, -1) + COLOR_VERTICAL_TAB_JOINED = (82, -1) COLOR_VERTICAL_TAB_CURRENT = (7, 4) COLOR_VERTICAL_TAB_NEW_MESSAGE = (5, -1) - COLOR_VERTICAL_TAB_HIGHLIGHT = (1, -1) + COLOR_VERTICAL_TAB_HIGHLIGHT = (3, -1) COLOR_VERTICAL_TAB_PRIVATE = (2, -1) + COLOR_VERTICAL_TAB_ATTENTION = (1, -1) COLOR_VERTICAL_TAB_DISCONNECTED = (8, -1) # Nickname colors @@ -161,8 +173,8 @@ class Theme(object): CHAR_JOIN = '--->' CHAR_QUIT = '<---' CHAR_KICK = '-!-' - CHAR_COLUMN_ASC = ' ▲' - CHAR_COLUMN_DESC =' ▼' + CHAR_COLUMN_ASC = ' ▲' + CHAR_COLUMN_DESC =' ▼' COLOR_JOIN_CHAR = (4, -1) COLOR_QUIT_CHAR = (1, -1) @@ -270,8 +282,8 @@ def reload_theme(): file_path = os.path.join(themes_dir, theme_name)+'.py' log.debug('Theme file to load: %s' %(file_path,)) new_theme = imp.load_source('theme', os.path.join(themes_dir, theme_name)+'.py') - except: - return 'Theme not found' + except Exception as e: + return 'Failed to load theme: %s' % (e,) theme = new_theme.theme if __name__ == '__main__': diff --git a/src/windows.py b/src/windows.py index 68518086..b8d276f0 100644 --- a/src/windows.py +++ b/src/windows.py @@ -199,6 +199,10 @@ class UserList(Win): 'none': lambda: get_theme().COLOR_USER_NONE, '': lambda: get_theme().COLOR_USER_NONE } + self.symbol_affiliation = {'owner': lambda: get_theme().CHAR_AFFILIATION_OWNER, + 'admin': lambda: get_theme().CHAR_AFFILIATION_ADMIN, + 'member': lambda: get_theme().CHAR_AFFILIATION_MEMBER, + 'none': lambda: get_theme().CHAR_AFFILIATION_NONE, } self.color_show = {'xa': lambda: get_theme().COLOR_STATUS_XA, 'none': lambda: get_theme().COLOR_STATUS_NONE, '': lambda: get_theme().COLOR_STATUS_NONE, @@ -206,7 +210,6 @@ class UserList(Win): 'away': lambda: get_theme().COLOR_STATUS_AWAY, 'chat': lambda: get_theme().COLOR_STATUS_CHAT } - def scroll_up(self): self.pos += self.height-1 @@ -222,30 +225,24 @@ class UserList(Win): log.debug('Refresh: %s',self.__class__.__name__) with g_lock: self._win.erase() - y = 0 - users = sorted(users) + if config.get('user_list_sort', 'desc').lower() == 'asc': + y, x = self._win.getmaxyx() + y -= 1 + users = sorted(users, reverse=True) + else: + y = 0 + users = sorted(users) + if self.pos >= len(users) and self.pos != 0: self.pos = len(users)-1 for user in users[self.pos:]: - if not user.role in self.color_role: - role_col = get_theme().COLOR_USER_NONE - else: - role_col = self.color_role[user.role]() - if not user.show in self.color_show: - show_col = get_theme().COLOR_STATUS_NONE + self.draw_role_affiliation(y, user) + self.draw_status_chatstate(y, user) + self.addstr(y, 2, user.nick[:self.width-2], to_curses_attr(user.color)) + if config.get('user_list_sort', 'desc').lower() == 'asc': + y -= 1 else: - show_col = self.color_show[user.show]() - if user.chatstate == 'composing': - char = get_theme().CHAR_CHATSTATE_COMPOSING - elif user.chatstate == 'active': - char = get_theme().CHAR_CHATSTATE_ACTIVE - elif user.chatstate == 'paused': - char = get_theme().CHAR_CHATSTATE_PAUSED - else: - char = get_theme().CHAR_STATUS - self.addstr(y, 0, char, to_curses_attr(show_col)) - self.addstr(y, 1, user.nick[:self.width-2], to_curses_attr(role_col)) - y += 1 + y += 1 if y == self.height: break # draw indicators of position in the list @@ -255,6 +252,29 @@ class UserList(Win): self.draw_plus(self.height-1) self._refresh() + def draw_role_affiliation(self, y, user): + if not user.role in self.color_role: + color = get_theme().COLOR_USER_NONE + else: + color = self.color_role[user.role]() + symbol = self.symbol_affiliation.get(user.affiliation, lambda: '-')() + self.addstr(y, 1, symbol, to_curses_attr(color)) + + def draw_status_chatstate(self, y, user): + if not user.show in self.color_show: + show_col = get_theme().COLOR_STATUS_NONE + else: + show_col = self.color_show[user.show]() + if user.chatstate == 'composing': + char = get_theme().CHAR_CHATSTATE_COMPOSING + elif user.chatstate == 'active': + char = get_theme().CHAR_CHATSTATE_ACTIVE + elif user.chatstate == 'paused': + char = get_theme().CHAR_CHATSTATE_PAUSED + else: + char = get_theme().CHAR_STATUS + self.addstr(y, 0, char, to_curses_attr(show_col)) + def resize(self, height, width, y, x): with g_lock: self._resize(height, width, y, x) @@ -302,7 +322,7 @@ class GlobalInfoBar(Win): for tab in sorted_tabs: color = tab.color if config.get('show_inactive_tabs', 'true') == 'false' and\ - color == get_theme().COLOR_TAB_NORMAL: + color is get_theme().COLOR_TAB_NORMAL: continue try: self.addstr("%s" % str(tab.nb), to_curses_attr(color)) @@ -334,7 +354,7 @@ class VerticalGlobalInfoBar(Win): sorted_tabs = sorted(self.core.tabs, key=comp) if config.get('show_inactive_tabs', 'true') == 'false': sorted_tabs = [tab for tab in sorted_tabs if\ - tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL] + tab.vertical_color is not get_theme().COLOR_VERTICAL_TAB_NORMAL] nb_tabs = len(sorted_tabs) if nb_tabs >= height: for y, tab in enumerate(sorted_tabs): @@ -1659,8 +1679,6 @@ class ListWin(Win): if not lines: return self.lines += lines - self.refresh() - curses.doupdate() def get_selected_row(self): """ diff --git a/src/xhtml.py b/src/xhtml.py index e7a045fa..cf7a5fc0 100644 --- a/src/xhtml.py +++ b/src/xhtml.py @@ -194,7 +194,9 @@ def get_body_from_message_stanza(message): if config.get('enable_xhtml_im', 'true') == 'true': xhtml_body = message['xhtml_im'] if xhtml_body: - return xhtml_to_poezio_colors(xhtml_body) + content = xhtml_to_poezio_colors(xhtml_body) + content = content if content else message['body'] + return content or " " return message['body'] def ncurses_color_to_html(color): @@ -288,9 +290,9 @@ def xhtml_to_poezio_colors(text): for elem in elems: if elem.tag == '{http://www.w3.org/1999/xhtml}a': if 'href' in elem.attrib and elem.attrib['href'] != elem.text: - message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text)) + message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text if elem.text else "")) else: - message += '\x19u' + elem.text + '\x19o' + message += '\x19u' + (elem.text if elem.text else "") + '\x19o' elif elem.tag == '{http://www.w3.org/1999/xhtml}blockquote': message += '“' elif elem.tag == '{http://www.w3.org/1999/xhtml}body': |