diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/core.py | 156 | ||||
-rw-r--r-- | src/plugin.py | 19 | ||||
-rw-r--r-- | src/plugin_manager.py | 5 | ||||
-rw-r--r-- | src/tabs.py | 119 |
4 files changed, 253 insertions, 46 deletions
diff --git a/src/core.py b/src/core.py index 1f93324d..ce675f04 100644 --- a/src/core.py +++ b/src/core.py @@ -121,20 +121,21 @@ class Core(object): 'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), self.completion_win), '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': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark 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)"), None), - '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>."), 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.'), None), + 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark 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), + '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), + '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.'), None), - 'version': (self.command_version, _('Usage: /version <jid>\nVersion: Get the software version of the given JID (usually its XMPP client and Operating System).'), None), + '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), 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: Disconnect from the remote server if you are currently connected and then connect to it again.'), None), - 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: Disconnect and reconnect in all the rooms in domain.'), None), + '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.'), 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), } self.key_func = { @@ -1208,8 +1209,25 @@ class Core(object): log.debug(_("Could not send directed presence:\n") + traceback.format_exc()) def completion_status(self, the_input): + """ + Completion of /status + """ return the_input.auto_completion([status for status in possible_show], ' ') + def completion_presence(self, the_input): + """ + Completion of /presence + """ + text = the_input.get_text() + args = text.split() + n = len(args) + if text.endswith(' '): + n += 1 + if n == 2: + return the_input.auto_completion([contact.bare_jid for contact in roster.get_contacts()], '') + elif n == 3: + return the_input.auto_completion([status for status in possible_show], '') + def command_load(self, arg): """ /load <plugin> @@ -1296,6 +1314,7 @@ class Core(object): self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received) def command_theme(self, arg): + """/theme <theme name>""" args = arg.split() if len(args) == 1: self.command_set('theme %s' % (args[0],)) @@ -1304,6 +1323,23 @@ class Core(object): self.information(warning, 'Warning') self.refresh_window() + def completion_theme(self, the_input): + """ Completion for /theme""" + themes_dir = config.get('themes_dir', '') + themes_dir = themes_dir or\ + os.path.join(os.environ.get('XDG_DATA_HOME') or\ + os.path.join(os.environ.get('HOME'), '.local', 'share'), + 'poezio', 'themes') + try: + names = os.listdir(themes_dir) + except OSError as e: + log.debug(_('Completion failed: %s') % e) + return + theme_files = [name[:-3] for name in names if name.endswith('.py')] + if not 'default' in theme_files: + theme_files.append('default') + return the_input.auto_completion(theme_files, '') + def command_win(self, arg): """ /win <number> @@ -1375,6 +1411,41 @@ class Core(object): the_input.auto_completion(serv_list, '') return True + def completion_bookmark(self, the_input): + """Completion for /bookmark""" + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + + if len(args) == 1: + jid = JID('') + else: + jid = JID(args[1]) + if jid.server and (jid.resource or jid.full.endswith('/')): + tab = self.get_tab_by_name(jid.bare, tabs.MucTab) + nicks = [tab.own_nick] if tab else [] + default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + nick = config.get('default_nick', '') + if not nick: + if not default in nicks: + nicks.append(default) + else: + if not nick in nicks: + nicks.append(nick) + jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] + return the_input.auto_completion(jids_list, '') + muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)] + return the_input.auto_completion(muc_list, '') + + def completion_version(self, the_input): + """Completion for /version""" + n = len(the_input.get_text().split()) + if n > 2 or (n == 2 and the_input.get_text().endswith(' ')): + return + return the_input.auto_completion([contact.bare_jid for contact in roster.get_contacts()], '') + def completion_list(self, the_input): muc_serv_list = [] for tab in self.tabs: # TODO, also from an history @@ -1519,6 +1590,77 @@ class Core(object): msg = "%s=%s" % (option, value) self.information(msg, 'Info') + def completion_server_cycle(self, the_input): + """Completion for /server_cycle""" + txt = the_input.get_text() + args = txt.split() + n = len(args) + if txt.endswith(' '): + n += 1 + if n == 2: + serv_list = [] + for tab in self.tabs: + if isinstance(tab, tabs.MucTab): + serv = JID(tab.get_name()).server + if not serv in serv_list: + 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 diff --git a/src/plugin.py b/src/plugin.py index 4dd88697..92adbc4b 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -1,24 +1,27 @@ import os -from configparser import ConfigParser +from configparser import RawConfigParser import config import inspect import traceback class PluginConfig(config.Config): - def __init__(self, filename): - ConfigParser.__init__(self) - self.__config_file__ = filename + def __init__(self, filename, module_name): + self.file_name = filename + self.module_name = module_name + RawConfigParser.__init__(self, None) self.read() def read(self): """Read the config file""" - ConfigParser.read(self, self.__config_file__) + RawConfigParser.read(self, self.file_name) + if not self.has_section(self.module_name): + self.add_section(self.module_name) def write(self): """Write the config to the disk""" try: - fp = open(self.__config_file__, 'w') - ConfigParser.write(self, fp) + fp = open(self.file_name, 'w') + RawConfigParser.write(self, fp) fp.close() return True except IOError: @@ -59,7 +62,7 @@ class BasePlugin(object, metaclass=SafetyMetaclass): SafetyMetaclass.core = core self.plugin_manager = plugin_manager conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg') - self.config = PluginConfig(conf) + self.config = PluginConfig(conf, self.__module__) self.init() def init(self): diff --git a/src/plugin_manager.py b/src/plugin_manager.py index fe4d2b7e..437d8ee2 100644 --- a/src/plugin_manager.py +++ b/src/plugin_manager.py @@ -99,12 +99,15 @@ class PluginManager(object): def add_tab_command(self, module_name, tab_type, name, handler, help, completion=None): commands = self.tab_commands[module_name] t = tab_type.__name__ + if name in tab_type.plugin_commands: + return if not t in commands: commands[t] = [] commands[t].append((name, handler, help, completion)) + tab_type.plugin_commands[name] = (handler, help, completion) for tab in self.core.tabs: if isinstance(tab, tab_type): - tab.add_plugin_command(name, handler, help, completion) + tab.update_commands() def del_tab_command(self, module_name, tab_type, name): commands = self.tab_commands[module_name] diff --git a/src/tabs.py b/src/tabs.py index 472a15fa..feb4be37 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -31,6 +31,7 @@ import singleton import xhtml import weakref import timed_events +import os import multiuserchat as muc @@ -222,17 +223,10 @@ class Tab(object): def on_input(self, key): pass - def add_plugin_command(self, name, handler, help, completion=None): - if name in self.plugin_commands or name in self.commands: - return - self.plugin_commands[name] = (handler, help, completion) - self.commands[name] = (handler, help, completion) - self.update_commands() - def update_commands(self): for c in self.plugin_commands: if not c in self.commands: - self.commands[name] = self.plugin_commands[c] + self.commands[c] = self.plugin_commands[c] def on_lose_focus(self): """ @@ -310,6 +304,8 @@ class ChatTab(Tab): _("""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.chat_state = None self.update_commands() @@ -364,6 +360,15 @@ class ChatTab(Tab): self.refresh() msg.send() + def command_clear(self, args): + """ + /clear + """ + self._text_buffer.messages = [] + self.text_win.rebuild_everything(self._text_buffer) + self.refresh() + self.core.doupdate() + def send_chat_state(self, state, always_send=False): """ Send an empty chatstate message @@ -467,27 +472,67 @@ class MucTab(ChatTab): self.key_func['M-u'] = self.scroll_user_list_down self.key_func['M-y'] = self.scroll_user_list_up # commands - self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname."), None) + 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."), None) - 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."), None) - 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."), None) + 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['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.'), None) + 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) 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."), 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.'), None) 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.'), 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_ignore) 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).'), None) 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.commands['clear'] = (self.command_clear, - _('Usage: /clear\nClear: Clear the current buffer.'), None) self.resize() self.update_commands() + def completion_nick(self, the_input): + """Completion for /nick""" + nicks = [os.environ.get('USER'), config.get('default_nick', ''), self.core.get_bookmark_nickname(self.get_name())] + while nicks.count(''): + nicks.remove('') + return the_input.auto_completion(nicks, '') + + def completion_ignore(self, the_input): + """Completion for /ignore""" + userlist = [user.nick for user in self.users] + userlist.remove(self.own_nick) + return the_input.auto_completion(userlist, '') + + def completion_role(self, the_input): + """Completion for /role""" + text = the_input.get_text() + args = common.shell_split(text) + n = len(args) + if text.endswith(' '): + n += 1 + if n == 2: + userlist = [user.nick for user in self.users] + userlist.remove(self.own_nick) + return the_input.auto_completion(userlist, '') + elif n == 3: + possible_roles = ['none', 'visitor', 'participant', 'moderator'] + return the_input.auto_completion(possible_roles, '') + + def completion_affiliation(self, the_input): + """Completion for /affiliation""" + text = the_input.get_text() + args = common.shell_split(text) + n = len(args) + if text.endswith(' '): + n += 1 + if n == 2: + userlist = [user.nick for user in self.users] + return the_input.auto_completion(userlist, '') + elif n == 3: + possible_affiliations = ['none', 'member', 'admin', 'owner'] + return the_input.auto_completion(possible_affiliations, '') + def scroll_user_list_up(self): self.user_win.scroll_up() self.user_win.refresh(self.users) @@ -533,15 +578,6 @@ class MucTab(ChatTab): self.core.xmpp.plugin['xep_0045'].configureRoom(self.get_name(), form) self.core.close_tab() - def command_clear(self, args): - """ - /clear - """ - self._text_buffer.messages = [] - self.text_win.rebuild_everything(self._text_buffer) - self.refresh() - self.core.doupdate() - def command_cycle(self, arg): if self.joined: muc.leave_groupchat(self.core.xmpp, self.get_name(), self.own_nick, arg) @@ -705,10 +741,9 @@ class MucTab(ChatTab): else: if len(args) > 1: msg = ' '+args[1] - self.core.information("-%s-" % msg) else: msg = '' - self.command_role(args[0]+ ' none'+msg) + self.command_role('"'+args[0]+ '" none'+msg) def command_role(self, arg): """ @@ -1457,8 +1492,8 @@ class RosterInfoTab(Tab): self.commands['groupadd'] = (self.command_groupadd, _("Usage: /groupadd <jid> <group>\nAdd the given JID to the given group."), self.completion_groupadd) 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['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."), None) - 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."), 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.resize() self.update_commands() @@ -1481,6 +1516,30 @@ class RosterInfoTab(Tab): not self.input.help_message: self.complete_commands(self.input) + def completion_file(self, the_input): + """ + Completion for /import and /export + """ + text = the_input.get_text() + args = text.split() + n = len(args) + if n == 1: + home = os.getenv('HOME') or '/' + return the_input.auto_completion([home, '/tmp'], '') + else: + the_path = text[text.index(' ')+1:] + try: + names = os.listdir(the_path) + except: + names = [] + end_list = [] + for name in names: + value = os.path.join(the_path, name) + if not name.startswith('.'): + end_list.append(value) + + return the_input.auto_completion(end_list, '') + def command_clear_infos(self, arg): """ /clear_infos |