command completion. Also completion for command's arguments. (try it to complete the server of the muc with /join test@kiko[TAB] for example

This commit is contained in:
louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13 2010-11-25 03:04:03 +00:00
parent c1a62ac897
commit 435bdd6e85
3 changed files with 168 additions and 85 deletions

View file

@ -34,6 +34,8 @@ import common
import theme
import logging
from sleekxmpp.xmlstream.stanzabase import JID
log = logging.getLogger(__name__)
import multiuserchat as muc
@ -93,28 +95,33 @@ class Core(object):
self.resize_timer = None
self.previous_tab_nb = 0
self.own_nick = config.get('own_nick', self.xmpp.boundjid.bare)
# global commands, available from all tabs (having an input, of course)
# global commands, available from all tabs
# a command is tuple of the form:
# (the function executing the command. Takes a string as argument,
# a string representing the help message,
# 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'),
'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")),
'quit': (self.command_quit, _("Usage: /quit\nQuit: Just disconnect from the server and exit poezio.")),
'exit': (self.command_quit, _("Usage: /exit\nExit: Just disconnect from the server and exit poezio.")),
'next': (self.rotate_rooms_right, _("Usage: /next\nNext: Go to the next room.")),
'n': (self.rotate_rooms_right, _("Usage: /n\nN: Go to the next room.")),
'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room.")),
'p': (self.rotate_rooms_left, _("Usage: /p\nP: Go to the previous room.")),
'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room.")),
'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room.")),
'show': (self.command_show, _("Usage: /show <availability> [status]\nShow: Change your availability and (optionaly) your status. The <availability> argument is one of \"avail, available, ok, here, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message")),
'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optional) sets your status message. This is equivalent to '/show away [message]'")),
'busy': (self.command_busy, _("Usage: /busy [message]\nBusy: Sets your availability to busy and (optional) sets your status message. This is equivalent to '/show busy [message]'")),
'avail': (self.command_avail, _("Usage: /avail [message]\nAvail: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'")),
'available': (self.command_avail, _("Usage: /available [message]\nAvailable: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'")),
'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 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)")),
'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Sets 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>.")),
'link': (self.command_link, _("Usage: /link [option] [number]\nLink: Interact with a link in the conversation. Available options are 'open', 'copy'. Open just opens the link in the browser if it's http://, Copy just copy the link in the clipboard. An optional number can be provided, it indicates which link to interact with.")),
'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.')),
'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.')),
'help': (self.command_help, '\_o< KOIN KOIN KOIN', None),
'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),
'next': (self.rotate_rooms_right, _("Usage: /next\nNext: Go to the next room."), None),
'n': (self.rotate_rooms_right, _("Usage: /n\nN: Go to the next room."), None),
'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room."), None),
'p': (self.rotate_rooms_left, _("Usage: /p\nP: Go to the previous room."), None),
'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room."), None),
'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), None),
'show': (self.command_show, _("Usage: /show <availability> [status]\nShow: Change your availability and (optionaly) your status. The <availability> argument is one of \"avail, available, ok, here, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message"), None),
'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optional) sets your status message. This is equivalent to '/show away [message]'"), None),
'busy': (self.command_busy, _("Usage: /busy [message]\nBusy: Sets your availability to busy and (optional) sets your status message. This is equivalent to '/show busy [message]'"), None),
'avail': (self.command_avail, _("Usage: /avail [message]\nAvail: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'"), None),
'available': (self.command_avail, _("Usage: /available [message]\nAvailable: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'"), 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 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: Sets 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>."), None),
'link': (self.command_link, _("Usage: /link [option] [number]\nLink: Interact with a link in the conversation. Available options are 'open', 'copy'. Open just opens the link in the browser if it's http://, Copy just copy the link in the clipboard. An optional number can be provided, it indicates which link to interact with."), None),
'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.'), None),
'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.'), None),
}
self.key_func = {
@ -146,7 +153,6 @@ class Core(object):
self.xmpp.add_event_handler("roster_update", self.on_roster_update)
self.xmpp.add_event_handler("changed_status", self.on_presence)
def grow_information_win(self):
"""
"""
@ -908,6 +914,28 @@ class Core(object):
self.current_tab().on_gain_focus()
self.refresh_window()
def completion_join(self, the_input):
"""
Try to complete the server of the MUC's jid (for now only from the currently
open ones)
TODO: have a history of recently joined MUCs, and use that too
"""
txt = the_input.get_text()
if len(txt.split()) != 2:
# we are not on the 1st argument of the command line
return False
jid = JID(txt.split()[1])
if not jid.user or not jid.server or jid.resource != '':
# we are not writing the server part of the jid
return True
serv = jid.server
serv_list = []
for tab in self.tabs:
if isinstance(tab, MucTab):
serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host))
the_input.auto_completion(serv_list, '')
return True
def command_join(self, arg):
"""
/join [room][/nick] [password]
@ -1211,21 +1239,9 @@ class Core(object):
else:
self.command_say(line)
# def command_say(self, line):
# if isinstance(self.current_tab(), PrivateTab):
# muc.send_private_message(self.xmpp, self.current_tab().get_name(), line)
# elif isinstance(self.current_tab(), ConversationTab): # todo, special case # hu, I can't remember what special case was needed when I wrote that…
# if isinstance(self.current_tab(), PrivateTab) or\
# isinstance(self.current_tab(), ConversationTab):
# self.add_message_to_text_buffer(self.current_tab().get_room(), line, None, self.own_nick)
# elif isinstance(self.current_tab(), MucTab):
# muc.send_groupchat_message(self.xmpp, self.current_tab().get_name(), line)
# self.doupdate()
def doupdate(self):
self.current_tab().just_before_refresh()
curses.doupdate()
# # global core object
core = Core(connection)

View file

@ -59,11 +59,39 @@ class Tab(object):
# and use them in on_input
self.commands = {} # and their own commands
def refresh(self, tabs, informations, roster):
def complete_commands(self, the_input):
"""
Called on each screen refresh (when something has changed)
Does command completion on the specified input for both global and tab-specific
commands.
This should be called from the completion method (on tab, for example), passing
the input where completion is to be made.
It can completion the command name itself or an argument of the command.
Returns True if a completion was made, False else.
"""
raise NotImplementedError
txt = the_input.get_text()
# check if this is a command
if txt.startswith('/') and not txt.startswith('//'):
# check if we are in the middle of the command name
if len(txt.split()) > 1 or\
(txt.endswith(' ') and not the_input.last_completion):
command_name = txt.split()[0][1:]
if command_name in self.core.commands:
command = self.core.commands[command_name]
elif command_name in self.commands:
command = self.commands[command_name]
else: # Unknown command, cannot complete
return False
if command[2] is None:
return False # There's no completion functio
else:
return command[2](the_input)
else:
# complete the command's name
words = ['/%s'%(name) for name in list(self.core.commands.keys())] +\
['/%s'% (name) for name in list(self.commands.keys())]
the_input.auto_completion(words, '')
return True
return False
def resize(self):
self.size = (self.height, self.width) = self.core.stdscr.getmaxyx()
@ -72,6 +100,12 @@ class Tab(object):
else:
self.visible = True
def refresh(self, tabs, informations, roster):
"""
Called on each screen refresh (when something has changed)
"""
raise NotImplementedError
def get_color_state(self):
"""
returns the color that should be used in the GlobalInfoBar
@ -224,7 +258,7 @@ class ChatTab(Tab):
self.key_func['\n'] = 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 '/'"""))
Useful if you want your message to begin with a '/'"""), None)
def last_words_completion(self):
"""
@ -242,7 +276,7 @@ class ChatTab(Tab):
for word in msg.txt.split():
if len(word) >= 4 and word not in words:
words.append(word)
self.input.auto_completion(words, False)
self.input.auto_completion(words, ' ')
def on_enter(self):
txt = self.input.key_enter()
@ -282,14 +316,14 @@ class MucTab(ChatTab):
self.key_func['^I'] = self.completion
self.key_func['M-i'] = self.completion
# commands
self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname."))
self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."))
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.commands['topic'] = (self.command_topic, _("Usage: /topic <subject>\nTopic: Change the subject of the room"))
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.commands['part'] = (self.command_part, _("Usage: /part [message]\n Part: disconnect from a room. You can specify an optional message."))
self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room"))
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.commands['ignore'] = (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname."), None)
self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list."), 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."), None)
self.commands['topic'] = (self.command_topic, _("Usage: /topic <subject>\nTopic: Change the subject of the room"), 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'), None)
self.commands['part'] = (self.command_part, _("Usage: /part [message]\n Part: disconnect from a room. You can specify an optional message."), None)
self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room"), None)
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.resize()
def command_recolor(self, arg):
@ -478,8 +512,17 @@ class MucTab(ChatTab):
"""
Called when Tab is pressed, complete the nickname in the input
"""
if self.complete_commands(self.input):
return
compare_users = lambda x: x.last_talked
self.input.auto_completion([user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)])
word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)]
after = config.get('after_completion', ',')+" "
if ' ' not in self.input.get_text() or (self.input.last_completion and\
self.input.get_text()[:-len(after)] == self.input.last_completion):
add_after = after
else:
add_after = ' '
self.input.auto_completion(word_list, add_after)
def get_color_state(self):
return self._room.color_state
@ -531,10 +574,17 @@ class PrivateTab(ChatTab):
self.info_win = windows.TextWin()
self.tab_win = windows.GlobalInfoBar()
self.input = windows.MessageInput()
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"))
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"))
# keys
self.key_func['^I'] = self.completion
self.key_func['M-i'] = self.completion
# commands
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None)
self.resize()
def completion(self):
self.complete_commands(self.input)
def command_say(self, line):
muc.send_private_message(self.core.xmpp, self.get_name(), line)
self.core.add_message_to_text_buffer(self.get_room(), line, None, self.get_room().own_nick)
@ -631,6 +681,16 @@ class RosterInfoTab(Tab):
self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show")
self.input = self.default_help_message
self.set_color_state(theme.COLOR_TAB_NORMAL)
self.key_func['^I'] = self.completion
self.key_func['M-i'] = self.completion
self.key_func["^J"] = self.on_enter
self.key_func["^M"] = self.on_enter
self.key_func[' '] = self.on_space
self.key_func["/"] = self.on_slash
self.key_func["KEY_UP"] = self.move_cursor_up
self.key_func["KEY_DOWN"] = self.move_cursor_down
self.key_func["o"] = self.toggle_offline_show
self.key_func["^F"] = self.start_search
self.resize()
def resize(self):
@ -644,6 +704,12 @@ class RosterInfoTab(Tab):
self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, self.core.stdscr)
self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr)
def completion(self):
# Check if we are entering a command (with the '/' key)
if isinstance(self.input, windows.CommandInput) and\
not self.input.help_message:
self.complete_commands(self.input)
def refresh(self, tabs, informations, roster):
if not self.visible:
return
@ -664,22 +730,11 @@ class RosterInfoTab(Tab):
self._color_state = color
def on_input(self, key):
key_commands = {
"^J": self.on_enter,
"^M": self.on_enter,
"\n": self.on_enter,
' ': self.on_space,
"/": self.on_slash,
"KEY_UP": self.move_cursor_up,
"KEY_DOWN": self.move_cursor_down,
"o": self.toggle_offline_show,
"^F": self.start_search,
}
res = self.input.do_command(key)
if res:
return True
if key in key_commands:
return key_commands[key]()
if key in self.key_func:
return self.key_func[key]()
def toggle_offline_show(self):
"""
@ -793,10 +848,17 @@ class ConversationTab(ChatTab):
self.info_win = windows.TextWin()
self.tab_win = windows.GlobalInfoBar()
self.input = windows.MessageInput()
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"))
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"))
# keys
self.key_func['^I'] = self.completion
self.key_func['M-i'] = self.completion
# commands
self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None)
self.resize()
def completion(self):
self.complete_commands(self.input)
def command_say(self, line):
muc.send_private_message(self.core.xmpp, self.get_name(), line)
self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick)

View file

@ -803,17 +803,20 @@ class Input(Win):
self.rewrite_text()
return True
def auto_completion(self, user_list, add_after=True):
def auto_completion(self, word_list, add_after):
"""
Complete the nickname
Complete the input, from a list of words
if add_after is None, we use the value defined in completion
plus a space, after the completion. If it's a string, we use it after the
completion (with no additional space)
"""
if self.pos+self.line_pos != len(self.text): # or len(self.text) == 0
return # we don't complete if cursor is not at the end of line
completion_type = config.get('completion', 'normal')
if completion_type == 'shell' and self.text != '':
self.shell_completion(user_list, add_after)
self.shell_completion(word_list, add_after)
else:
self.normal_completion(user_list, add_after)
self.normal_completion(word_list, add_after)
return True
def reset_completion(self):
@ -823,46 +826,48 @@ class Input(Win):
self.hit_list = []
self.last_completion = None
def normal_completion(self, user_list, add_after):
def normal_completion(self, word_list, after):
"""
Normal completion
"""
if add_after and (" " not in self.text.strip() or\
self.last_completion and self.text == self.last_completion+config.get('after_completion', ',')+" "):
after = config.get('after_completion', ',')+" "
#if " " in self.text.strip() and (not self.last_completion or ' ' in self.last_completion):
else:
after = " " # don't put the "," if it's not the begining of the sentence
(y, x) = self._win.getyx()
if not self.last_completion:
# begin is the begining of the nick we want to complete
if self.text.strip() != '':
# if self.text.strip() != '' and\
# not self.text.endswith(after):
if self.text.strip():
begin = self.text.split()[-1].lower()
else:
begin = ''
# else:
# begin = ''
hit_list = [] # list of matching nicks
for user in user_list:
if user.lower().startswith(begin):
hit_list.append(user)
for word in word_list:
if word.lower().startswith(begin):
hit_list.append(word)
if len(hit_list) == 0:
return
self.hit_list = hit_list
end = len(begin)
else:
begin = self.text[-len(after)-len(self.last_completion):-len(after)]
if after:
begin = self.text[-len(after)-len(self.last_completion):-len(after)]
else:
begin = self.last_completion
self.hit_list.append(self.hit_list.pop(0)) # rotate list
end = len(begin) + len(after)
self.text = self.text[:-end]
if end:
self.text = self.text[:-end]
nick = self.hit_list[0] # take the first hit
self.last_completion = nick
self.text += nick +after
self.key_end(False)
def shell_completion(self, user_list, add_after):
def shell_completion(self, word_list, after):
"""
Shell-like completion
"""
if " " in self.text.strip() or not add_after:
if " " in self.text.strip() or add_after is not None:
after = " " # don't put the "," if it's not the begining of the sentence
else:
after = config.get('after_completion', ',')+" "
@ -872,7 +877,7 @@ class Input(Win):
else:
begin = ''
hit_list = [] # list of matching nicks
for user in user_list:
for user in word_list:
if user.lower().startswith(begin):
hit_list.append(user)
if len(hit_list) == 0: