From 2ea8673a0408bb3151ea8fdb4045814d88e770bb Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Sun, 6 Jan 2013 17:36:14 +0100
Subject: Improve the help system (#1986)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

TODO: change the plugin API to take advantage of this
This change is backwards-compatible (as in “should not break anything”)
---
 src/core.py    | 218 ++++++++++++++++++++++++++++++++++++----------
 src/tabs.py    | 269 +++++++++++++++++++++++++++++++++++++++++++++------------
 src/theming.py |   5 ++
 3 files changed, 390 insertions(+), 102 deletions(-)

(limited to 'src')

diff --git a/src/core.py b/src/core.py
index 646e8188..3d5f8268 100644
--- a/src/core.py
+++ b/src/core.py
@@ -102,6 +102,7 @@ possible_show = {'available':None,
                  }
 
 Status = collections.namedtuple('Status', 'show message')
+Command = collections.namedtuple('Command', 'func desc comp short usage')
 
 class Core(object):
     """
@@ -156,42 +157,8 @@ class Core(object):
         #  a completion function, taking a Input as argument. Can be None)
         #  The completion function should return True if a completion was
         #  made ; False otherwise
-        self.commands = {
-            'help': (self.command_help, '\_o< KOIN KOIN KOIN', self.completion_help),
-            'join': (self.command_join, _("Usage: /join [room_name][@server][/nick] [password]\nJoin: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join room2\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / password"), self.completion_join),
-            'exit': (self.command_quit, _("Usage: /exit\nExit: Just disconnect from the server and exit poezio."), None),
-            'quit': (self.command_quit, _("Usage: /quit\nQuit: Just disconnect from the server and exit poezio."), None),
-            'next': (self.rotate_rooms_right, _("Usage: /next\nNext: Go to the next room."), None),
-            'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room."), None),
-            'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room."), self.completion_win),
-            'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), self.completion_win),
-            'move_tab': (self.command_move_tab, _("Usage: /move_tab <source> <destination>\nMove Tab: Insert the <source> tab at the position of <destination>. This will make the following tabs shift in some cases (refer to the documentation). A tab can be designated by its number or by the beginning of its address."), self.completion_move_tab),
-            'show': (self.command_status, _('Usage: /show <availability> [status message]\nShow: 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),
-            '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] [password]\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 [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`. `toggle` can be used as a special value to toggle a boolean option."), self.completion_set),
-            '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),
-            'version': (self.command_version, _('Usage: /version <jid>\nVersion: Get the software version of the given JID (usually its XMPP client and Operating System).'), self.completion_version),
-            'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: Disconnect and reconnect in all the rooms in domain.'), self.completion_server_cycle),
-            'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: Bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same as the Up key.'), None),
-            'load': (self.command_load, _('Usage: /load <plugin>\nLoad: Load the specified plugin'), self.plugin_manager.completion_load),
-            'unload': (self.command_unload, _('Usage: /unload <plugin>\nUnload: Unload the specified plugin'), self.plugin_manager.completion_unload),
-            '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),
-            '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),
-            'runkey': (self.command_runkey, _("Usage: /runkey <key>\nRunkey: Execute the action defined for <key>."), self.completion_runkey),
-            'self': (self.command_self, _("Usage: /self\nSelf: Remind you of who you are."), None),
-            'activity': (self.command_activity, _("Usage: /activity <jid>\nActivity: Informs you of the last activity of a JID."), self.completion_activity),
-        }
+        self.commands = {}
+        self.register_initial_commands()
 
         # We are invisible
         if config.get('send_initial_presence', 'true').lower() == 'false':
@@ -1324,27 +1291,55 @@ class Core(object):
 
 ####################### Commands and completions ##############################
 
+    def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''):
+        if name in self.commands:
+            return
+        if not desc and shortdesc:
+            desc = shortdesc
+        self.commands[name] = Command(func, desc, completion, shortdesc, usage)
+
     def command_help(self, arg):
         """
         /help <command_name>
         """
         args = arg.split()
-        acc = []
         if not args:
-            msg = _('Available commands are: ')
+            color = get_theme().COLOR_HELP_COMMANDS[0]
+            acc = []
+            buff = ['Global commands:']
             for command in self.commands:
-               acc.append(command)
-            for command in self.current_tab().commands:
-               acc.append(command)
-            msg += ' '.join(sorted(acc))
+                if isinstance(self.commands[command], Command):
+                    acc.append('  \x19%s}%s\x19o - %s' % (color, command, self.commands[command].short))
+                else:
+                    acc.append('  \x19%s}%s\x19o' % (color, command))
+            acc = sorted(acc)
+            buff.extend(acc)
+            acc = []
+            buff.append('Tab-specific commands:')
+            commands = self.current_tab().commands
+            for command in commands:
+                if isinstance(commands[command], Command):
+                    acc.append('  \x19%s}%s\x19o - %s' % (color, command, commands[command].short))
+                else:
+                    acc.append('  \x19%s}%s\x19o' % (color, command))
+            acc = sorted(acc)
+            buff.extend(acc)
+
+            msg = '\n'.join(buff)
             msg += _("\nType /help <command_name> to know what each command does")
         if args:
             if args[0] in self.commands:
-                msg = self.commands[args[0]][1]
+                tup = self.commands[args[0]]
             elif args[0] in self.current_tab().commands:
-                msg = self.current_tab().commands[args[0]][1]
+                tup = self.current_tab().commands[args[0]]
+            else:
+                self.information(_('Unknown command: %s') % args[0], 'Error')
+                return
+            if isinstance(tup, Command):
+                msg = _('Usage: /%s %s\n' % (args[0], tup.usage))
+                msg += tup.desc
             else:
-                msg = _('Unknown command: %s') % args[0]
+                msg = tup[1]
         self.information(msg, 'Help')
 
     def completion_help(self, the_input):
@@ -2326,6 +2321,139 @@ class Core(object):
                 nick)
         self.information(info, 'Info')
 
+    def register_initial_commands(self):
+        """
+        Register the commands when poezio starts
+        """
+        self.register_command('help', self.command_help,
+                usage=_('[command]'),
+                shortdesc='\_o< KOIN KOIN KOIN',
+                completion=self.completion_help)
+        self.register_command('join', self.command_join,
+                usage=_("[room_name][@server][/nick] [password]"),
+                desc=_("Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join room2\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / password"),
+                shortdesc=_('Join a room'),
+                completion=self.completion_join)
+        self.register_command('exit', self.command_quit,
+                desc=_('Just disconnect from the server and exit poezio.'),
+                shortdesc=_('Exit poezio.'))
+        self.register_command('quit', self.command_quit,
+                desc=_('Just disconnect from the server and exit poezio.'),
+                shortdesc=_('Exit poezio.'))
+        self.register_command('next', self.rotate_rooms_right,
+                shortdesc=_('Go to the next room.'))
+        self.register_command('prev', self.rotate_rooms_left,
+                shortdesc=_('Go to the previous room.'))
+        self.register_command('win', self.command_win,
+                usage=_('<number or name>'),
+                shortdesc=_('Go to the specified room'),
+                completion=self.completion_win)
+        self.commands['w'] = self.commands['win']
+        self.register_command('move_tab', self.command_move_tab,
+                usage=_('<source> <destination>'),
+                desc=_('Insert the <source> tab at the position of <destination>. This will make the following tabs shift in some cases (refer to the documentation). A tab can be designated by its number or by the beginning of its address.'),
+                shortdesc=_('Move a tab.'),
+                completion=self.completion_move_tab)
+        self.register_command('show', self.command_status,
+                usage=_('<availability> [status message]'),
+                desc=_('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.'),
+                shortdesc=_('Change your availability.'),
+                completion=self.completion_status)
+        self.commands['status'] = self.commands['show']
+        self.register_command('bookmark_local', self.command_bookmark_local,
+                usage=_("[roomname][/nick] [password]"),
+                desc=_("Bookmark 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)"),
+                shortdesc=_('Bookmark a room locally.'),
+                completion=self.completion_bookmark_local)
+        self.register_command('bookmark', self.command_bookmark,
+                usage=_("[roomname][/nick] [autojoin] [password]"),
+                desc=_("\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)"),
+                shortdesc=_("Bookmark a room online."),
+                completion=self.completion_bookmark)
+        self.register_command('set', self.command_set,
+                usage=_("[plugin|][section] <option> [value]"),
+                desc=_("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`. `toggle` can be used as a special value to toggle a boolean option."),
+                shortdesc=_("Set the value of an option"),
+                completion=self.completion_set)
+        self.register_command('theme', self.command_theme,
+                usage=_('[theme name]'),
+                desc=_('Reload the theme defined in the config file. If theme_name is provided, set that theme before reloading it.'),
+                shortdesc=_('Load a theme'),
+                completion=self.completion_theme)
+        self.register_command('list', self.command_list,
+                usage=_('[server]'),
+                desc=_('Get the list of public chatrooms on the specified server.'),
+                shortdesc=_('List the rooms.'),
+                completion=self.completion_list)
+        self.register_command('message', self.command_message,
+                usage=_('<jid> [optional message]'),
+                desc=_('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.'),
+                shortdesc=_('Send a message'),
+                completion=self.completion_version)
+        self.register_command('version', self.command_version,
+                usage='<jid>',
+                desc=_('Get the software version of the given JID (usually its XMPP client and Operating System).'),
+                shortdesc=_('Get the software version of a JID.'),
+                completion=self.completion_version)
+        self.register_command('server_cycle', self.command_server_cycle,
+                usage=_('[domain] [message]'),
+                desc=_('Disconnect and reconnect in all the rooms in domain.'),
+                shortdesc=_('Cycle a range of rooms'),
+                completion=self.completion_server_cycle)
+        self.register_command('bind', self.command_bind,
+                usage=_(' <key> <equ>'),
+                desc=_('Bind a key to another key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same as the Up key.'),
+                shortdesc=_('Bind a key to another key.'))
+        self.register_command('load', self.command_load,
+                usage=_('<plugin>'),
+                shortdesc=_('Load the specified plugin'),
+                completion=self.plugin_manager.completion_load)
+        self.register_command('unload', self.command_unload,
+                usage=_('<plugin>'),
+                shortdesc=_('Unload the specified plugin'),
+                completion=self.plugin_manager.completion_unload)
+        self.register_command('plugins', self.command_plugins,
+                shortdesc=_('Show the plugins in use.'))
+        self.register_command('presence', self.command_presence,
+                usage=_('<JID> [type] [status]'),
+                desc=_('Send a directed presence to <JID> and using [type] and [status] if provided.'),
+                shortdesc=_('Send a directed presence.'),
+                completion=self.completion_presence)
+        self.register_command('rawxml', self.command_rawxml,
+                usage='<xml>',
+                shortdesc=_('Send a custom xml stanza.'))
+        self.register_command('invite', self.command_invite,
+                usage=_('<jid> <room> [reason]'),
+                desc=_('Invite jid in room with reason.'),
+                shortdesc=_('Invite someone in a room.'),
+                completion=self.completion_invite)
+        self.register_command('decline', self.command_decline,
+                usage=_('<room> [reason]'),
+                desc=_('Decline the invitation to room with or without reason.'),
+                shortdesc=_('Decline an invitation.'),
+                completion=self.completion_decline)
+        self.register_command('invitations', self.command_invitations,
+                shortdesc=_('Show the pending invitations.'))
+        self.register_command('bookmarks', self.command_bookmarks,
+                shortdesc=_('Show the current bookmarks.'))
+        self.register_command('remove_bookmark', self.command_remove_bookmark,
+                usage='[jid]',
+                desc=_('Remove the specified bookmark, or the bookmark on the current tab, if any.'),
+                shortdesc=_('Remove a bookmark'),
+                completion=self.completion_remove_bookmark)
+        self.register_command('xml_tab', self.command_xml_tab,
+                shortdesc=_('Open an XML tab.'))
+        self.register_command('runkey', self.command_runkey,
+                usage=_('<key>'),
+                shortdesc=_('Execute the action defined for <key>.'),
+                completion=self.completion_runkey)
+        self.register_command('self', self.command_self,
+                shortdesc=_('Remind you of who you are.'))
+        self.register_command('activity', self.command_activity,
+                usage='<jid>',
+                desc=_('Informs you of the last activity of a JID.'),
+                shortdesc=_('Get the activity of someone.'),
+                completion=self.completion_activity)
 
 ####################### XMPP Event Handlers  ##################################
 
diff --git a/src/tabs.py b/src/tabs.py
index 01266263..166c9e25 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -101,6 +101,7 @@ STATE_PRIORITY = {
 
 class Tab(object):
     tab_core = None
+
     def __init__(self):
         self.input = None
         if isinstance(self, MucTab) and not self.joined:
@@ -188,6 +189,16 @@ class Tab(object):
             Tab.visible = True
         windows.Win._tab_win = scr
 
+    def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''):
+        """
+        Add a command
+        """
+        if name in self.commands:
+            return
+        if not desc and shortdesc:
+            desc = shortdesc
+        self.commands[name] = core.Command(func, desc, completion, shortdesc, usage)
+
     def complete_commands(self, the_input):
         """
         Does command completion on the specified input for both global and tab-specific
@@ -429,14 +440,18 @@ class ChatTab(Tab):
         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,
-                                 _("""Usage: /say <message>\nSay: Just send the message.
-                                        Useful if you want your message to begin with a '/'."""), None)
-        self.commands['xhtml'] =  (self.command_xhtml, _("Usage: /xhtml <custom xhtml>\nXHTML: Send custom XHTML."), None)
-        self.commands['clear'] =  (self.command_clear,
-                                 _('Usage: /clear\nClear: Clear the current buffer.'), None)
-        self.commands['correct'] = (self.command_correct, _('Usage: /correct\nCorrect: Fix the last message with whatever you want.'), self.completion_correct)
-
+        self.register_command('say', self.command_say,
+                usage=_('<message>'),
+                shortdesc=_('Send the message.'))
+        self.register_command('xhtml', self.command_xhtml,
+                usage=_('<custom xhtml>'),
+                shortdesc=_('Send custom XHTML.'))
+        self.register_command('clear', self.command_clear,
+                shortdesc=_('Clear the current buffer.'))
+        self.register_command('correct', self.command_correct,
+                desc=_('Fix the last message with whatever you want.'),
+                shortdesc=_('Correct the last message.'),
+                completion=self.completion_correct)
         self.chat_state = None
         self.update_commands()
         self.update_keys()
@@ -678,23 +693,83 @@ class MucTab(ChatTab):
         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)
-        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_quoted)
-        self.commands['ban'] =  (self.command_ban, _("Usage: /ban <nick> [reason]\nBan: ban the user with the specified nickname. You also can give an optional reason."), self.completion_quoted)
-        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 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_quoted)
-        self.commands['part'] = (self.command_part, _("Usage: /part [message]\nPart: Disconnect from a room. You can specify an optional message."), None)
-        self.commands['close'] = (self.command_close, _("Usage: /close [message]\nClose: Disconnect from a room and close the tab. You can specify an optional message if you are still connected."), None)
-        self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room."), self.completion_nick)
-        self.commands['recolor'] = (self.command_recolor, _('Usage: /recolor\nRecolor: Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'), self.completion_recolor)
-        self.commands['cycle'] = (self.command_cycle, _('Usage: /cycle [message]\nCycle: Leave the current room and rejoin it immediately.'), None)
-        self.commands['info'] = (self.command_info, _('Usage: /info <nickname>\nInfo: Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'), self.completion_info)
-        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)
+        self.register_command('ignore', self.command_ignore,
+                usage=_('<nickname>'),
+                desc=_('Ignore a specified nickname.'),
+                shortdesc=_('Ignore someone'),
+                completion=self.completion_ignore)
+        self.register_command('unignore', self.command_unignore,
+                usage=_('<nickname>'),
+                desc=_('Remove the specified nickname from the ignore list.'),
+                shortdesc=_('Unignore someone.'),
+                completion=self.completion_unignore)
+        self.register_command('kick', self.command_kick,
+                usage=_('<nick> [reason]'),
+                desc=_('Kick the user with the specified nickname. You also can give an optional reason.'),
+                shortdesc=_('Kick someone.'),
+                completion=self.completion_quoted)
+        self.register_command('ban', self.command_ban,
+                usage=_('<nick> [reason]'),
+                desc=_('Ban the user with the specified nickname. You also can give an optional reason.'),
+                shortdesc='Ban someone',
+                completion=self.completion_quoted)
+        self.register_command('role', self.command_role,
+                usage=_('<nick> <role> [reason]'),
+                desc=_('Set the role of an user. Roles can be: none, visitor, participant, moderator. You also can give an optional reason.'),
+                shortdesc=_('Set the role of an user.'),
+                completion=self.completion_role)
+        self.register_command('affiliation', self.command_affiliation,
+                usage=_('<nick or jid> <affiliation>'),
+                desc=_('Set the affiliation of an user. Affiliations can be: outcast, none, member, admin, owner.'),
+                shortdesc=_('Set the affiliation of an user.'),
+                completion=self.completion_affiliation)
+        self.register_command('topic', self.command_topic,
+                usage=_('<subject>'),
+                desc=_('Change the subject of the room.'),
+                shortdesc=_('Change the subject.'),
+                completion=self.completion_topic)
+        self.register_command('query', self.command_query,
+                usage=_('<nick> [message]'),
+                desc=_('Query: 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.'),
+                shortdesc=_('Query an user.'),
+                completion=self.completion_quoted)
+        self.register_command('part', self.command_part,
+                usage=_('[message]'),
+                desc=_('Disconnect from a room. You can specify an optional message.'),
+                shortdesc=_('Leave the room.'))
+        self.register_command('close', self.command_close,
+                usage=_('[message]'),
+                desc=_('Disconnect from a room and close the tab. You can specify an optional message if you are still connected.'),
+                shortdesc=_('Close the tab.'))
+        self.register_command('nick', self.command_nick,
+                usage=_('<nickname>'),
+                desc=_('Change your nickname in the current room.'),
+                shortdesc=_('Change your nickname.'),
+                completion=self.completion_nick)
+        self.register_command('recolor', self.command_recolor,
+                desc=_('Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'),
+                shortdesc=_('Change the nicks colors.'),
+                completion=self.completion_recolor)
+        self.register_command('cycle', self.command_cycle,
+                usage=_('[message]'),
+                desc=_('Leave the current room and rejoin it immediately.'),
+                shortdesc=_('Leave and re-join the room.'))
+        self.register_command('info', self.command_info,
+                usage=_('<nickname>'),
+                desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
+                shortdesc=_('Show an user\'s infos.'),
+                completion=self.completion_info)
+        self.register_command('configure', self.command_configure,
+                desc=_('Configure the current room, through a form.'),
+                shortdesc=_('Configure the room.'))
+        self.register_command('version', self.command_version,
+                usage=_('<jid or nick>'),
+                desc=_('Get the software version of the given JID or nick in room (usually its XMPP client and Operating System).'),
+                shortdesc=_('Get the software version of a jid.'),
+                completion=self.completion_version)
+        self.register_command('names', self.command_names,
+                desc=_('Get the list of the users in the room, and the list of the people assuming the different roles.'),
+                shortdesc=_('List the users.'))
 
         if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
             del self.commands["nick"]
@@ -1680,10 +1755,16 @@ class PrivateTab(ChatTab):
         # keys
         self.key_func['^I'] = self.completion
         # commands
-        self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'), None)
-        self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: Close the tab."), None)
-        self.commands['close'] = (self.command_unquery, _("Usage: /close\nClose: Close the tab."), None)
-        self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: Get the software version of the current interlocutor (usually its XMPP client and Operating System).'), None)
+        self.register_command('info', self.command_info,
+                desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
+                shortdesc=_('Info about the user.'))
+        self.register_command('unquery', self.command_unquery,
+                shortdesc=_('Close the tab.'))
+        self.register_command('close', self.command_unquery,
+                shortdesc=_('Close the tab.'))
+        self.register_command('version', self.command_version,
+                desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
+                shortdesc=_('Get the software version of a jid.'))
         self.resize()
         self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab)
         self.on = True
@@ -1975,19 +2056,64 @@ class RosterInfoTab(Tab):
         self.key_func["n"] = self.change_contact_name
         self.key_func["s"] = self.start_search
         self.key_func["S"] = self.start_search_slow
-        self.commands['deny'] = (self.command_deny, _("Usage: /deny [jid]\nDeny: Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster."), self.completion_deny)
-        self.commands['accept'] = (self.command_accept, _("Usage: /accept [jid]\nAccept: Allow the provided JID (or the selected contact in your roster), to see your presence."), self.completion_deny)
-        self.commands['add'] = (self.command_add, _("Usage: /add <jid>\nAdd: Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence."), None)
-        self.commands['name'] = (self.command_name, _("Usage: /name <jid> <name>\nSet the given JID's name."), self.completion_name)
-        self.commands['groupadd'] = (self.command_groupadd, _("Usage: /groupadd <jid> <group>\nAdd the given JID to the given group."), self.completion_groupadd)
-        self.commands['groupmove'] = (self.command_groupmove, _("Usage: /groupchange <jid> <old group> <new group>\nMoves the given JID from the old group to the new group."), self.completion_groupmove)
-        self.commands['groupremove'] = (self.command_groupremove, _("Usage: /groupremove <jid> <group>\nRemove the given JID from the given group."), self.completion_groupremove)
-        self.commands['remove'] = (self.command_remove, _("Usage: /remove [jid]\nRemove: Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster."), self.completion_remove)
-        self.commands['reconnect'] = (self.command_reconnect, _('Usage: /reconnect\nConnect: Disconnect from the remote server if you are currently connected and then connect to it again.'), None)
-        self.commands['export'] = (self.command_export, _("Usage: /export [/path/to/file]\nExport: Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not."), self.completion_file)
-        self.commands['import'] = (self.command_import, _("Usage: /import [/path/to/file]\nImport: Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not."), self.completion_file)
-        self.commands['clear_infos'] = (self.command_clear_infos, _("Usage: /clear_infos\nClear Infos: Use this command to clear the info buffer."), None)
-        self.commands['activity'] = (self.command_activity, _("Usage: /activity <jid>\nActivity: Informs you of the last activity of a JID."), self.core.completion_activity)
+        self.register_command('deny', self.command_deny,
+                usage=_('[jid]'),
+                desc=_('Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster.'),
+                shortdesc=_('Deny an user your presence.'),
+                completion=self.completion_deny)
+        self.register_command('accept', self.command_accept,
+                usage=_('[jid]'),
+                desc=_('Allow the provided JID (or the selected contact in your roster), to see your presence.'),
+                shortdesc=_('Allow an user your presence.'),
+                completion=self.completion_deny)
+        self.register_command('add', self.command_add,
+                usage=_('<jid>'),
+                desc=_('Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence.'),
+                shortdesc=_('Add an user to your roster.'))
+        self.register_command('name', self.command_name,
+                usage=_('<jid> <name>'),
+                shortdesc=_('Set the given JID\'s name.'),
+                completion=self.completion_name)
+        self.register_command('groupadd', self.command_groupadd,
+                usage=_('<jid> <group>'),
+                desc=_('Add the given JID to the given group.'),
+                shortdesc=_('Add an user to a group'),
+                completion=self.completion_groupadd)
+        self.register_command('groupmove', self.command_groupmove,
+                usage=_('<jid> <old group> <new group>'),
+                desc=_('Move the given JID from the old group to the new group.'),
+                shortdesc=_('Move an user to another group.'),
+                completion=self.completion_groupmove)
+        self.register_command('groupremove', self.command_groupremove,
+                usage=_('<jid> <group>'),
+                desc=_('Remove the given JID from the given group.'),
+                shortdesc=_('Remove an user from a group.'),
+                completion=self.completion_groupremove)
+        self.register_command('remove', self.command_remove,
+                usage=_('[jid]'),
+                desc=_('Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster.'),
+                shortdesc=_('Remove an user from your roster.'),
+                completion=self.completion_remove)
+        self.register_command('reconnect', self.command_reconnect,
+                desc=_('Disconnect from the remote server if you are currently connected and then connect to it again.'),
+                shortdesc=_('Disconnect and reconnect to the server.'))
+        self.register_command('export', self.command_export,
+                usage=_('[/path/to/file]'),
+                desc=_('Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not.'),
+                shortdesc=_('Export your roster to a file.'),
+                completion=self.completion_file)
+        self.register_command('import', self.command_import,
+                usage=_('[/path/to/file]'),
+                desc=_('Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not.'),
+                shortdesc=_('Import your roster from a file.'),
+                completion=self.completion_file)
+        self.register_command('clear_infos', self.command_clear_infos,
+                shortdesc=_('Clear the info buffer.'))
+        self.register_command('activity', self.command_activity,
+                usage=_('<jid>'),
+                desc=_('Informs you of the last activity of a JID.'),
+                shortdesc=_('Get the activity of someone.'),
+                completion=self.core.completion_activity)
         self.core.xmpp.add_event_handler('session_start',
                 lambda event: self.core.xmpp.plugin['xep_0030'].get_info(
                     jid=self.core.xmpp.boundjid.domain,
@@ -2001,9 +2127,16 @@ class RosterInfoTab(Tab):
         if iq['type'] == 'error':
             return
         if 'urn:xmpp:blocking' in iq['disco_info'].get_features():
-            self.commands['block'] = (self.command_block, _("Usage: /block [jid]\nBlock: prevent a JID from talking to you."), self.completion_block)
-            self.commands['unblock'] = (self.command_unblock, _("Usage: /unblock [jid]\nUnblock: allow a JID to talk to you."), self.completion_unblock)
-            self.commands['list_blocks'] = (self.command_list_blocks, _("Usage: /list_blocks\nList Blocks: Retrieve the list of the blocked contacts."), None)
+            self.register_command('block', self.command_block,
+                    usage=_('[jid]'),
+                    shortdesc=_('Prevent a JID from talking to you.'),
+                    completion=self.completion_block)
+            self.register_command('unblock', self.command_unblock,
+                    usage=_('[jid]'),
+                    shortdesc=_('Allow a JID to talk to you.'),
+                    completion=self.completion_unblock)
+            self.register_command('list_blocks', self.command_list_blocks,
+                    shortdesc=_('Show the blocked contacts.'))
             self.core.xmpp.del_event_handler('session_start', self.check_blocking)
             self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
         else:
@@ -2763,11 +2896,20 @@ class ConversationTab(ChatTab):
         # keys
         self.key_func['^I'] = self.completion
         # commands
-        self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: Close the tab."), None)
-        self.commands['close'] = (self.command_unquery, _("Usage: /close\Close: Close the tab."), None)
-        self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: Get the software version of the current interlocutor (usually its XMPP client and Operating System).'), None)
-        self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Get the status of the contact.'), None)
-        self.commands['activity'] = (self.command_activity, _('Usage: /activity [jid]\nActivity: Get the last activity of the given or the current contact.'), self.core.completion_activity)
+        self.register_command('unquery', self.command_unquery,
+                shortdesc=_('Close the tab.'))
+        self.register_command('close', self.command_unquery,
+                shortdesc=_('Close the tab.'))
+        self.register_command('version', self.command_version,
+                desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
+                shortdesc=_('Get the software version of the user.'))
+        self.register_command('info', self.command_info,
+                shortdesc=_('Get the status of the contact.'))
+        self.register_command('activity', self.command_activity,
+                usage=_('[jid]'),
+                desc=_('Get the last activity of the given or the current contact.'),
+                shortdesc=_('Get the activity.'),
+                completion=self.core.completion_activity)
         self.resize()
         self.update_commands()
         self.update_keys()
@@ -3058,7 +3200,8 @@ class MucListTab(Tab):
         self.key_func['KEY_LEFT'] = self.list_header.sel_column_left
         self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right
         self.key_func[' '] = self.sort_by
-        self.commands['close'] = (self.close, _("Usage: /close\nClose: Just close this tab."), None)
+        self.register_command('close', self.close,
+                shortdesc=_('Close this tab.'))
         self.resize()
         self.update_keys()
         self.update_commands()
@@ -3206,12 +3349,24 @@ class XMLTab(Tab):
         self.text_win = windows.TextWin()
         self.core.xml_buffer.add_window(self.text_win)
         self.default_help_message = windows.HelpText("/ to enter a command")
-        self.commands['close'] = (self.close, _("Usage: /close\nClose: Just close this tab."), None)
-        self.commands['clear'] = (self.command_clear, _("Usage: /clear\nClear: Clear the content of the current buffer."), None)
-        self.commands['reset'] = (self.command_reset, _("Usage: /reset\nReset: Reset the stanza filter."), None)
-        self.commands['filter_id'] = (self.command_filter_id, _("Usage: /filter_id <id>\nFilterId: Show only the stanzas with the id <id>."), None)
-        self.commands['filter_xpath'] = (self.command_filter_xpath, _("Usage: /filter_xpath <xpath>\nFilterXPath: Show only the stanzas matching the xpath <xpath>."), None)
-        self.commands['filter_xmlmask'] = (self.command_filter_xmlmask, _("Usage: /filter_xmlmask <xml mask>\nFilterXMLMask: Show only the stanzas matching the given xml mask."), None)
+        self.register_command('close', self.close,
+                shortdesc=_("Close this tab."))
+        self.register_command('clear', self.command_clear,
+                shortdesc=_('Clear the current buffer.'))
+        self.register_command('reset', self.command_reset,
+                shortdesc=_('Reset the stanza filter.'))
+        self.register_command('filter_id', self.command_filter_id,
+                usage='<id>',
+                desc=_('Show only the stanzas with the id <id>.'),
+                shortdesc=_('Filter by id.'))
+        self.register_command('filter_xpath', self.command_filter_xpath,
+                usage='<xpath>',
+                desc=_('Show only the stanzas matching the xpath <xpath>.'),
+                shortdesc=_('Filter by XPath.'))
+        self.register_command('filter_xmlmask', self.command_filter_xmlmask,
+                usage=_('<xml mask>'),
+                desc=_('Show only the stanzas matching the given xml mask.'),
+                shortdesc=_('Filter by xml mask.'))
         self.input = self.default_help_message
         self.key_func['^T'] = self.close
         self.key_func['^I'] = self.completion
diff --git a/src/theming.py b/src/theming.py
index 8e394a40..07a37c6e 100644
--- a/src/theming.py
+++ b/src/theming.py
@@ -80,6 +80,10 @@ class Theme(object):
     # Message text color
     COLOR_NORMAL_TEXT = (-1, -1)
     COLOR_INFORMATION_TEXT = (5, -1) # TODO
+
+    # Color of the commands in the help message
+    COLOR_HELP_COMMANDS = (208, -1)
+
     # "reverse" is a special value, available only for this option. It just
     # takes the nick colors and reverses it. A theme can still specify a
     # fixed color if need be.
@@ -205,6 +209,7 @@ class Theme(object):
             'error': (16, 1),
             'warning': (1, 16),
             'roster': (2, 16),
+            'help': (10, -1),
             'headline': (11, -1, 'b'),
             'default': (7, -1),
     }
-- 
cgit v1.2.3