summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/common.py96
-rw-r--r--src/core.py226
-rw-r--r--src/decorators.py13
-rw-r--r--src/plugin_manager.py4
-rw-r--r--src/shlex.py303
-rw-r--r--src/tabs.py201
-rw-r--r--src/windows.py116
7 files changed, 720 insertions, 239 deletions
diff --git a/src/common.py b/src/common.py
index d861fea4..25950987 100644
--- a/src/common.py
+++ b/src/common.py
@@ -182,7 +182,7 @@ def datetime_tuple(timestamp):
:return: The date.
:rtype: :py:class:`datetime.datetime`
- >>> common.datetime_tuple('20130226T06:23:12')
+ >>> datetime_tuple('20130226T06:23:12')
datetime.datetime(2013, 2, 26, 8, 23, 12)
"""
timestamp = timestamp.split('.')[0]
@@ -233,19 +233,82 @@ def shell_split(st):
>>> shell_split('"sdf 1" "toto 2"')
['sdf 1', 'toto 2']
"""
- sh = shlex.shlex(st, posix=True)
- sh.commenters = ''
- sh.whitespace_split = True
- sh.quotes = '"'
- ret = list()
- try:
+ sh = shlex.shlex(st)
+ ret = []
+ w = sh.get_token()
+ while w and w[2] is not None:
+ ret.append(w[2])
w = sh.get_token()
- while w is not None:
- ret.append(w)
- w = sh.get_token()
- return ret
- except ValueError:
- return st.split(" ")
+ return ret
+
+def find_argument(pos, text, quoted=True):
+ """
+ Split an input into a list of arguments, return the number of the
+ argument selected by pos.
+
+ If the position searched is outside the string, or in a space between words,
+ then it will return the position of an hypothetical new argument.
+
+ See the doctests of the two methods for example behaviors.
+
+ :param int pos: The position to search.
+ :param str text: The text to analyze.
+ :param quoted: Whether to take quotes into account or not.
+ :rtype: int
+ """
+ if quoted:
+ return find_argument_quoted(pos, text)
+ else:
+ return find_argument_unquoted(pos, text)
+
+def find_argument_quoted(pos, text):
+ """
+ >>> find_argument_quoted(4, 'toto titi tata')
+ 3
+ >>> find_argument_quoted(4, '"toto titi" tata')
+ 0
+ >>> find_argument_quoted(8, '"toto" "titi tata"')
+ 1
+ >>> find_argument_quoted(8, '"toto" "titi tata')
+ 1
+ >>> find_argument_quoted(18, '"toto" "titi tata" ')
+ 2
+ """
+ sh = shlex.shlex(text)
+ count = -1
+ w = sh.get_token()
+ while w and w[2] is not None:
+ count += 1
+ if w[0] <= pos < w[1]:
+ return count
+ w = sh.get_token()
+
+ return count + 1
+
+def find_argument_unquoted(pos, text):
+ """
+ >>> find_argument_unquoted(2, 'toto titi tata')
+ 0
+ >>> find_argument_unquoted(3, 'toto titi tata')
+ 0
+ >>> find_argument_unquoted(6, 'toto titi tata')
+ 1
+ >>> find_argument_unquoted(4, 'toto titi tata')
+ 3
+ >>> find_argument_unquoted(25, 'toto titi tata')
+ 3
+ """
+ ret = text.split()
+ search = 0
+ argnum = 0
+ for i, elem in enumerate(ret):
+ elem_start = text.find(elem, search)
+ elem_end = elem_start + len(elem)
+ search = elem_end
+ if elem_start <= pos < elem_end:
+ return i
+ argnum = i
+ return argnum + 1
def parse_str_to_secs(duration=''):
"""
@@ -286,7 +349,7 @@ def parse_secs_to_str(duration=0):
:rtype: :py:class:`str`
>>> parse_secs_to_str(3601)
- 1h1s
+ '1h1s'
"""
secs, mins, hours, days = 0, 0, 0, 0
result = ''
@@ -370,3 +433,8 @@ def safeJID(*args, **kwargs):
return JID(*args, **kwargs)
except InvalidJID:
return JID('')
+
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/src/core.py b/src/core.py
index 8edbd8dc..9589f8e4 100644
--- a/src/core.py
+++ b/src/core.py
@@ -1482,8 +1482,8 @@ class Core(object):
def completion_help(self, the_input):
"""Completion for /help."""
- commands = list(self.commands.keys()) + list(self.current_tab().commands.keys())
- return the_input.auto_completion(commands, ' ', quotify=False)
+ commands = sorted(self.commands.keys()) + sorted(self.current_tab().commands.keys())
+ return the_input.new_completion(commands, 1, quotify=False)
def command_runkey(self, arg):
"""
@@ -1509,7 +1509,7 @@ class Core(object):
list_ = []
list_.extend(self.key_func.keys())
list_.extend(self.current_tab().key_func.keys())
- return the_input.auto_completion(list_, '', quotify=False)
+ return the_input.new_completion(list_, 1, quotify=False)
def command_status(self, arg):
"""
@@ -1548,7 +1548,8 @@ class Core(object):
"""
Completion of /status
"""
- return the_input.auto_completion([status for status in possible_show], ' ', quotify=False)
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion([status for status in possible_show], 1, ' ', quotify=False)
def command_presence(self, arg):
"""
@@ -1595,15 +1596,11 @@ class Core(object):
"""
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([jid for jid in roster.jids()], '')
- elif n == 3:
- return the_input.auto_completion([status for status in possible_show], '')
+ arg = the_input.get_argument_position()
+ if arg == 1:
+ return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=True)
+ elif arg == 2:
+ return the_input.auto_completion([status for status in possible_show], '', quotify=True)
def command_theme(self, arg=''):
"""/theme <theme name>"""
@@ -1631,7 +1628,7 @@ class Core(object):
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, '', quotify=False)
+ return the_input.new_completion(theme_files, 1, '', quotify=False)
def command_win(self, arg):
"""
@@ -1675,7 +1672,7 @@ class Core(object):
for tab in self.tabs:
l.extend(tab.matching_names())
l = [i[1] for i in l]
- return the_input.auto_completion(l, ' ', quotify=False)
+ return the_input.new_completion(l, 1, '', quotify=False)
def command_move_tab(self, arg):
"""
@@ -1692,7 +1689,7 @@ class Core(object):
except ValueError:
old_tab = None
for tab in self.tabs:
- if not old_tab and value in safeJID(tab.get_name()).user:
+ if not old_tab and value == tab.get_name():
old_tab = tab
if not old_tab:
self.information("Tab %s does not exist" % args[0], "Error")
@@ -1712,8 +1709,11 @@ class Core(object):
def completion_move_tab(self, the_input):
"""Completion for /move_tab"""
- nodes = [safeJID(tab.get_name()).user for tab in self.tabs]
- return the_input.auto_completion(nodes, ' ', quotify=True)
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ nodes = [tab.get_name() for tab in self.tabs if tab]
+ nodes.remove('Roster')
+ return the_input.new_completion(nodes, 1, ' ', quotify=True)
def command_list(self, arg):
"""
@@ -1741,7 +1741,7 @@ class Core(object):
tab.get_name() not in muc_serv_list:
muc_serv_list.append(safeJID(tab.get_name()).server)
if muc_serv_list:
- return the_input.auto_completion(muc_serv_list, ' ', quotify=False)
+ return the_input.new_completion(muc_serv_list, 1, quotify=False)
def command_version(self, arg):
"""
@@ -1770,12 +1770,11 @@ class Core(object):
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(' ')):
+ n = the_input.get_argument_position(quoted=True)
+ if n >= 2:
return
- comp = reduce(lambda x, y: x + [i for i in y], (jid.resources for jid in roster if len(jid)), [])
- comp = (str(res.jid) for res in comp)
- return the_input.auto_completion(sorted(comp), '', quotify=False)
+ comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
+ return the_input.new_completion(sorted(comp), 1, '', quotify=True)
def command_join(self, arg, histo_length=None):
"""
@@ -1968,18 +1967,15 @@ class Core(object):
def completion_bookmark_local(self, the_input):
"""Completion for /bookmark_local"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
- if len(args) == 1:
- jid = safeJID('')
- else:
- jid = safeJID(args[1])
- if len(args) > 2:
+ if n >= 2:
return
+ if len(args) == 1:
+ args.append('')
+ jid = safeJID(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 []
@@ -1992,10 +1988,10 @@ class Core(object):
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, '')
+ return the_input.new_completion(jids_list, 1, quotify=True)
muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
muc_list.append('*')
- return the_input.auto_completion(muc_list, '')
+ return the_input.new_completion(muc_list, 1, quotify=True)
def command_bookmark(self, arg=''):
"""
@@ -2070,22 +2066,18 @@ class Core(object):
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 = safeJID('')
- else:
- jid = safeJID(args[1])
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
- if len(args) == 2:
- return the_input.auto_completion(['true', 'false'], '')
- if len(args) == 3:
+ if n == 2:
+ return the_input.new_completion(['true', 'false'], 2, quotify=True)
+ if n >= 3:
return
+ if len(args) == 1:
+ args.append('')
+ jid = safeJID(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 []
@@ -2098,10 +2090,11 @@ class Core(object):
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, '')
+ return the_input.new_completion(jids_list, 1, quotify=True)
muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
+ muc_list.sort()
muc_list.append('*')
- return the_input.auto_completion(muc_list, '')
+ return the_input.new_completion(muc_list, 1, quotify=True)
def command_bookmarks(self, arg=''):
"""/bookmarks"""
@@ -2133,7 +2126,7 @@ class Core(object):
def completion_remove_bookmark(self, the_input):
"""Completion for /remove_bookmark"""
- return the_input.auto_completion([bm.jid for bm in bookmark.bookmarks], '')
+ return the_input.new_completion([bm.jid for bm in bookmark.bookmarks], 1, quotify=False)
def command_set(self, arg):
"""
@@ -2170,27 +2163,24 @@ class Core(object):
def completion_set(self, the_input):
"""Completion for /set"""
- text = the_input.get_text()
- args = common.shell_split(text)
- n = len(args)
- empty = False
- if text.endswith(' '):
- n += 1
- empty = True
- if n == 2:
- if not empty and '|' in args[1]:
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
+ if n >= len(args):
+ args.append('')
+ if n == 1:
+ if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
- return the_input.auto_completion([],'')
+ return the_input.new_completion([], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()]
else:
end_list = config.options('Poezio')
- elif n == 3:
+ elif n == 2:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
- return the_input.auto_completion([''],'')
+ return the_input.auto_completion([''], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = plugin.config.options(section or plugin_name)
elif not config.has_option('Poezio', args[1]):
@@ -2201,11 +2191,11 @@ class Core(object):
end_list = []
else:
end_list = [config.get(args[1], ''), '']
- elif n == 4:
+ elif n == 3:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
- return the_input.auto_completion([],'')
+ return the_input.auto_completion([''], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = [plugin.config.get(args[2], '', section or plugin_name), '']
else:
@@ -2213,7 +2203,9 @@ class Core(object):
end_list = ['']
else:
end_list = [config.get(args[2], '', args[1]), '']
- return the_input.auto_completion(end_list, '')
+ else:
+ return
+ return the_input.new_completion(end_list, n, quotify=True)
def command_server_cycle(self, arg=''):
"""
@@ -2241,18 +2233,12 @@ class Core(object):
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 = set()
- for tab in self.tabs:
- if isinstance(tab, tabs.MucTab):
- serv = safeJID(tab.get_name()).server
- serv_list.add(serv)
- return the_input.auto_completion(list(serv_list), ' ')
+ serv_list = set()
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab):
+ serv = safeJID(tab.get_name()).server
+ serv_list.add(serv)
+ return the_input.new_completion(sorted(serv_list), 1, ' ')
def command_last_activity(self, arg):
"""
@@ -2284,7 +2270,7 @@ class Core(object):
self.xmpp.plugin['xep_0012'].get_last_activity(jid, block=False, callback=callback)
def completion_last_activity(self, the_input):
- return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=False)
+ return the_input.new_completion([jid for jid in roster.jids()], 1, quotify=False)
def command_mood(self, arg):
"""
@@ -2304,7 +2290,9 @@ class Core(object):
def completion_mood(self, the_input):
"""Completion for /mood"""
- return the_input.auto_completion(list(pep.MOODS.keys()), '', quotify=False)
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.new_completion(sorted(pep.MOODS.keys()), 1, quotify=True)
def command_activity(self, arg):
"""
@@ -2347,18 +2335,16 @@ class Core(object):
def completion_activity(self, the_input):
"""Completion for /activity"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion(list(pep.ACTIVITIES.keys()), '', quotify=False)
- elif n == 3:
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
+ if n == 1:
+ return the_input.new_completion(sorted(pep.ACTIVITIES.keys()), n, quotify=True)
+ elif n == 2:
if args[1] in pep.ACTIVITIES:
l = list(pep.ACTIVITIES[args[1]])
l.remove('category')
- return the_input.auto_completion(l, '', quotify=False)
+ l.sort()
+ return the_input.new_completion(l, n, quotify=True)
def command_invite(self, arg):
"""/invite <to> <room> [reason]"""
@@ -2372,19 +2358,16 @@ class Core(object):
def completion_invite(self, the_input):
"""Completion for /invite"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion([jid for jid in roster.jids()], '')
- elif n == 3:
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.new_completion(sorted(jid for jid in roster.jids()), n, quotify=True)
+ elif n == 2:
rooms = []
for tab in self.tabs:
if isinstance(tab, tabs.MucTab) and tab.joined:
rooms.append(tab.get_name())
- return the_input.auto_completion(rooms, '')
+ rooms.sort()
+ return the_input.new_completion(rooms, n, '', quotify=True)
def command_decline(self, arg):
"""/decline <room@server.tld> [reason]"""
@@ -2400,13 +2383,9 @@ class Core(object):
def completion_decline(self, the_input):
"""Completion for /decline"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion(list(self.pending_invites.keys()), '')
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.auto_completion(sorted(self.pending_invites.keys()), 1, '', quotify=True)
### Commands without a completion in this class ###
@@ -2442,6 +2421,20 @@ class Core(object):
self.reset_curses()
sys.exit()
+ def completion_bind(self, the_input):
+ n = the_input.get_argument_position()
+ if n == 1:
+ args = [key for key in self.key_func if not key.startswith('_')]
+ elif n == 2:
+ args = [key for key in self.key_func]
+ else:
+ return
+
+ return the_input.new_completion(args, n, '', quotify=False)
+
+
+ return the_input
+
def command_bind(self, arg):
"""
Bind a key.
@@ -2537,15 +2530,15 @@ class Core(object):
def completion_message(self, the_input):
"""Completion for /message"""
- n = len(the_input.get_text().split())
- if n > 2 or (n == 2 and the_input.get_text().endswith(' ')):
+ n = the_input.get_argument_position(quoted=True)
+ if n >= 2:
return
- comp = reduce(lambda x, y: x + [i for i in y], (jid.resources for jid in roster if len(jid)), [])
- comp = sorted((str(res.jid) for res in comp))
- bares = sorted(contact.bare_jid for contact in roster if len(contact))
- off = sorted(contact.bare_jid for contact in roster if not len(contact))
+ comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
+ comp = sorted(comp)
+ bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact]))
+ off = sorted(jid for jid in roster.jids() if jid not in bares)
comp = bares + comp + off
- return the_input.auto_completion(comp, '', quotify=True)
+ return the_input.new_completion(comp, 1, '', quotify=True)
def command_xml_tab(self, arg=''):
"""/xml_tab"""
@@ -2652,6 +2645,7 @@ class Core(object):
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.'),
+ completion=self.completion_bind,
shortdesc=_('Bind a key to another key.'))
self.register_command('load', self.command_load,
usage=_('<plugin>'),
@@ -3253,12 +3247,14 @@ class Core(object):
"""subscribe received"""
jid = presence['from'].bare
contact = roster[jid]
- if contact.subscription in ('from', 'both'):
+ if contact and contact.subscription in ('from', 'both'):
return
- elif contact.subscription == 'to':
+ elif contact and contact.subscription == 'to':
self.xmpp.sendPresence(pto=jid, ptype='subscribed')
self.xmpp.sendPresence(pto=jid)
else:
+ if not contact:
+ contact = roster.get_and_set(jid)
roster.update_contact_groups(contact)
contact.pending_in = True
self.information('%s wants to subscribe to your presence' % jid, 'Roster')
diff --git a/src/decorators.py b/src/decorators.py
index ab987701..3cb967b3 100644
--- a/src/decorators.py
+++ b/src/decorators.py
@@ -1,7 +1,9 @@
"""
-Module containing the decorators
+Module containing various decorators
"""
+from functools import partial
+
class RefreshWrapper(object):
def __init__(self):
self.core = None
@@ -40,5 +42,14 @@ class RefreshWrapper(object):
return ret
return wrap
+def __completion(quoted, func):
+ class Completion(object):
+ quoted = quoted
+ def __new__(cls, *args, **kwargs):
+ return func(*args, **kwargs)
+ return Completion
+
+completion_quotes = partial(__completion, True)
+completion_raw = partial(__completion, False)
refresh_wrapper = RefreshWrapper()
diff --git a/src/plugin_manager.py b/src/plugin_manager.py
index 9e253f97..5931d798 100644
--- a/src/plugin_manager.py
+++ b/src/plugin_manager.py
@@ -314,13 +314,13 @@ class PluginManager(object):
plugins_files = [name[:-3] for name in names if name.endswith('.py')
and name != '__init__.py' and not name.startswith('.')]
plugins_files.sort()
- return the_input.auto_completion(plugins_files, '', quotify=False)
+ return the_input.new_completion(plugins_files, 1, '', quotify=False)
def completion_unload(self, the_input):
"""
completion function that completes the name of the plugins that are loaded
"""
- return the_input.auto_completion(list(self.plugins.keys()), '', quotify=False)
+ return the_input.new_completion(sorted(self.plugins.keys()), 1, '', quotify=False)
def on_plugins_dir_change(self, new_value):
global plugins_dir
diff --git a/src/shlex.py b/src/shlex.py
new file mode 100644
index 00000000..87a241ed
--- /dev/null
+++ b/src/shlex.py
@@ -0,0 +1,303 @@
+"""
+A lexical analyzer class for simple shell-like syntaxes.
+
+Tweaked for the specific needs of parsing poezio input.
+
+"""
+
+# Module and documentation by Eric S. Raymond, 21 Dec 1998
+# Input stacking and error message cleanup added by ESR, March 2000
+# push_source() and pop_source() made explicit by ESR, January 2001.
+# Posix compliance, split(), string arguments, and
+# iterator interface by Gustavo Niemeyer, April 2003.
+
+import os
+import re
+import sys
+from collections import deque
+
+from io import StringIO
+
+__all__ = ["shlex", "split", "quote"]
+
+class shlex:
+ """
+ A custom version of the shlex in the stdlib to yield more information
+ """
+ def __init__(self, instream=None, infile=None, posix=True):
+ if isinstance(instream, str):
+ instream = StringIO(instream)
+ if instream is not None:
+ self.instream = instream
+ self.infile = infile
+ else:
+ self.instream = sys.stdin
+ self.infile = None
+ self.posix = posix
+ self.eof = ''
+ self.commenters = ''
+ self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
+ if self.posix:
+ self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'
+ 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ')
+ self.whitespace = ' \t\r\n'
+ self.whitespace_split = True
+ self.quotes = '"'
+ self.escape = '\\'
+ self.escapedquotes = '"'
+ self.state = ' '
+ self.pushback = deque()
+ self.lineno = 1
+ self.debug = 0
+ self.token = ''
+ self.filestack = deque()
+ self.source = None
+ if self.debug:
+ print('shlex: reading from %s, line %d' \
+ % (self.instream, self.lineno))
+
+ def push_token(self, tok):
+ "Push a token onto the stack popped by the get_token method"
+ if self.debug >= 1:
+ print("shlex: pushing token " + repr(tok))
+ self.pushback.appendleft(tok)
+
+ def push_source(self, newstream, newfile=None):
+ "Push an input source onto the lexer's input source stack."
+ if isinstance(newstream, str):
+ newstream = StringIO(newstream)
+ self.filestack.appendleft((self.infile, self.instream, self.lineno))
+ self.infile = newfile
+ self.instream = newstream
+ self.lineno = 1
+ if self.debug:
+ if newfile is not None:
+ print('shlex: pushing to file %s' % (self.infile,))
+ else:
+ print('shlex: pushing to stream %s' % (self.instream,))
+
+ def pop_source(self):
+ "Pop the input source stack."
+ self.instream.close()
+ (self.infile, self.instream, self.lineno) = self.filestack.popleft()
+ if self.debug:
+ print('shlex: popping to %s, line %d' \
+ % (self.instream, self.lineno))
+ self.state = ' '
+
+ def get_token(self):
+ "Get a token from the input stream (or from stack if it's nonempty)"
+ if self.pushback:
+ tok = self.pushback.popleft()
+ if self.debug >= 1:
+ print("shlex: popping token " + repr(tok))
+ return tok
+ # No pushback. Get a token.
+ start, end, raw = self.read_token()
+ # Handle inclusions
+ # Maybe we got EOF instead?
+ while raw == self.eof:
+ if not self.filestack:
+ return self.eof
+ else:
+ self.pop_source()
+ start, end, raw = self.get_token()
+ # Neither inclusion nor EOF
+ if self.debug >= 1:
+ if raw != self.eof:
+ print("shlex: token=" + repr(raw))
+ else:
+ print("shlex: token=EOF")
+ return start, end, raw
+
+ def read_token(self):
+ quoted = False
+ escapedstate = ' '
+ token_start = 0
+ token_end = -1
+ while True:
+ nextchar = self.instream.read(1)
+ if nextchar == '\n':
+ self.lineno = self.lineno + 1
+ if self.debug >= 3:
+ print("shlex: in state", repr(self.state), \
+ "I see character:", repr(nextchar))
+ if self.state is None:
+ self.token = '' # past end of file
+ token_end = self.instream.tell()
+ break
+ elif self.state == ' ':
+ if not nextchar:
+ self.state = None # end of file
+ token_end = self.instream.tell()
+ break
+ elif nextchar in self.whitespace:
+ if self.debug >= 2:
+ print("shlex: I see whitespace in whitespace state")
+ if self.token or (self.posix and quoted):
+ token_end = self.instream.tell() - 1
+ break # emit current token
+ else:
+ continue
+ elif nextchar in self.commenters:
+ self.instream.readline()
+ self.lineno = self.lineno + 1
+ elif self.posix and nextchar in self.escape:
+ token_start = self.instream.tell() - 1
+ escapedstate = 'a'
+ self.state = nextchar
+ elif nextchar in self.wordchars:
+ token_start = self.instream.tell() - 1
+ self.token = nextchar
+ self.state = 'a'
+ elif nextchar in self.quotes:
+ token_start = self.instream.tell() - 1
+ self.state = nextchar
+ elif self.whitespace_split:
+ token_start = self.instream.tell() - 1
+ self.token = nextchar
+ self.state = 'a'
+ else:
+ token_start = self.instream.tell() - 1
+ self.token = nextchar
+ if self.token or (self.posix and quoted):
+ token_end = self.instream.tell() - 1
+ break # emit current token
+ else:
+ continue
+ elif self.state in self.quotes:
+ quoted = True
+ if not nextchar: # end of file
+ if self.debug >= 2:
+ print("shlex: I see EOF in quotes state")
+ # XXX what error should be raised here?
+ token_end = self.instream.tell()
+ break
+ if nextchar == self.state:
+ if not self.posix:
+ self.token = self.token + nextchar
+ self.state = ' '
+ token_end = self.instream.tell()
+ break
+ else:
+ self.state = 'a'
+ elif self.posix and nextchar in self.escape and \
+ self.state in self.escapedquotes:
+ escapedstate = self.state
+ self.state = nextchar
+ else:
+ self.token = self.token + nextchar
+ elif self.state in self.escape:
+ if not nextchar: # end of file
+ if self.debug >= 2:
+ print("shlex: I see EOF in escape state")
+ # XXX what error should be raised here?
+ token_end = self.instream.tell()
+ break
+ # In posix shells, only the quote itself or the escape
+ # character may be escaped within quotes.
+ if escapedstate in self.quotes and \
+ nextchar != self.state and nextchar != escapedstate:
+ self.token = self.token + self.state
+ self.token = self.token + nextchar
+ self.state = escapedstate
+ elif self.state == 'a':
+ if not nextchar:
+ self.state = None # end of file
+ token_end = self.instream.tell()
+ break
+ elif nextchar in self.whitespace:
+ if self.debug >= 2:
+ print("shlex: I see whitespace in word state")
+ self.state = ' '
+ if self.token or (self.posix and quoted):
+ token_end = self.instream.tell() - 1
+ break # emit current token
+ else:
+ continue
+ elif self.posix and nextchar in self.quotes:
+ self.state = nextchar
+ elif self.posix and nextchar in self.escape:
+ escapedstate = 'a'
+ self.state = nextchar
+ elif nextchar in self.wordchars or nextchar in self.quotes \
+ or self.whitespace_split:
+ self.token = self.token + nextchar
+ else:
+ self.pushback.appendleft(nextchar)
+ if self.debug >= 2:
+ print("shlex: I see punctuation in word state")
+ self.state = ' '
+ if self.token:
+ token_end = self.instream.tell()
+ break # emit current token
+ else:
+ continue
+ result = self.token
+ self.token = ''
+ if self.posix and not quoted and result == '':
+ result = None
+ if self.debug > 1:
+ if result:
+ print("shlex: raw token=" + repr(result))
+ else:
+ print("shlex: raw token=EOF")
+ return (token_start, token_end, result)
+
+ def sourcehook(self, newfile):
+ "Hook called on a filename to be sourced."
+ if newfile[0] == '"':
+ newfile = newfile[1:-1]
+ # This implements cpp-like semantics for relative-path inclusion.
+ if isinstance(self.infile, str) and not os.path.isabs(newfile):
+ newfile = os.path.join(os.path.dirname(self.infile), newfile)
+ return (newfile, open(newfile, "r"))
+
+ def error_leader(self, infile=None, lineno=None):
+ "Emit a C-compiler-like, Emacs-friendly error-message leader."
+ if infile is None:
+ infile = self.infile
+ if lineno is None:
+ lineno = self.lineno
+ return "\"%s\", line %d: " % (infile, lineno)
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ token = self.get_token()
+ if token and token[0] == self.eof:
+ raise StopIteration
+ return token
+
+def split(s, comments=False, posix=True):
+ lex = shlex(s, posix=posix)
+ lex.whitespace_split = True
+ if not comments:
+ lex.commenters = ''
+ return list(lex)
+
+
+_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
+
+def quote(s):
+ """Return a shell-escaped version of the string *s*."""
+ if not s:
+ return "''"
+ if _find_unsafe(s) is None:
+ return s
+
+ # use single quotes, and put single quotes into double quotes
+ # the string $'b is then quoted as '$'"'"'b'
+ return "'" + s.replace("'", "'\"'\"'") + "'"
+
+
+if __name__ == '__main__':
+ lexer = shlex(instream=sys.argv[1])
+ while 1:
+ tt = lexer.get_token()
+ if tt:
+ print("Token: " + repr(tt))
+ else:
+ break
diff --git a/src/tabs.py b/src/tabs.py
index ceb69278..6a97e476 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -215,8 +215,29 @@ class Tab(object):
txt = the_input.get_text()
# check if this is a command
if txt.startswith('/') and not txt.startswith('//'):
+ position = the_input.get_argument_position(quoted=False)
+ if position == 0:
+ words = ['/%s'% (name) for name in sorted(self.core.commands)] +\
+ ['/%s' % (name) for name in sorted(self.commands)]
+ the_input.new_completion(words, 0)
+ # Do not try to cycle command completion if there was only
+ # one possibily. The next tab will complete the argument.
+ # Otherwise we would need to add a useless space before being
+ # able to complete the arguments.
+ hit_copy = set(the_input.hit_list)
+ while not hit_copy:
+ whitespace = the_input.text.find(' ')
+ if whitespace == -1:
+ whitespace = len(the_input.text)
+ the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:]
+ the_input.new_completion(words, 0)
+ hit_copy = set(the_input.hit_list)
+ if len(hit_copy) == 1:
+ the_input.do_command(' ')
+ the_input.reset_completion()
+ return True
# check if we are in the middle of the command name
- if len(txt.split()) > 1 or\
+ elif len(txt.split()) > 1 or\
(txt.endswith(' ') and not the_input.last_completion):
command_name = txt.split()[0][1:]
if command_name in self.commands:
@@ -229,22 +250,6 @@ class Tab(object):
return False # There's no completion function
else:
return command[2](the_input)
- else:
- # complete the command's name
- words = ['/%s'% (name) for name in self.core.commands] +\
- ['/%s' % (name) for name in self.commands]
- the_input.auto_completion(words, '', quotify=False)
- # Do not try to cycle command completion if there was only
- # one possibily. The next tab will complete the argument.
- # Otherwise we would need to add a useless space before being
- # able to complete the arguments.
- hit_copy = set(the_input.hit_list)
- while not hit_copy:
- the_input.key_backspace()
- the_input.auto_completion(words, '', quotify=False)
- hit_copy = set(the_input.hit_list)
- if len(hit_copy) == 1:
- the_input.do_command(' ')
return True
return False
@@ -637,7 +642,7 @@ class ChatTab(Tab):
self.command_say(line, correct=True)
def completion_correct(self, the_input):
- if self.last_sent_message:
+ if self.last_sent_message and the_input.get_argument_position() == 1:
return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False)
@property
@@ -834,15 +839,13 @@ class MucTab(ChatTab):
compare_users = lambda x: x.last_talked
userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
if user.nick != self.own_nick]
- contact_list = [jid for jid in roster.jids()]
- userlist.extend(contact_list)
- return the_input.auto_completion(userlist, '', quotify=False)
+ return the_input.auto_completion(userlist, quotify=False)
def completion_info(self, the_input):
"""Completion for /info"""
compare_users = lambda x: x.last_talked
userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)]
- return the_input.auto_completion(userlist, '', quotify=False)
+ return the_input.auto_completion(userlist, quotify=False)
def completion_nick(self, the_input):
"""Completion for /nick"""
@@ -851,7 +854,9 @@ class MucTab(ChatTab):
return the_input.auto_completion(nicks, '', quotify=False)
def completion_recolor(self, the_input):
- return the_input.auto_completion(['random'], '', quotify=False)
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion(['random'], 1, '', quotify=False)
+ return True
def completion_ignore(self, the_input):
"""Completion for /ignore"""
@@ -859,32 +864,24 @@ class MucTab(ChatTab):
if self.own_nick in userlist:
userlist.remove(self.own_nick)
userlist.sort()
- return the_input.auto_completion(userlist, '', quotify=False)
+ return the_input.auto_completion(userlist, quotify=False)
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:
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
userlist = [user.nick for user in self.users]
if self.own_nick in userlist:
userlist.remove(self.own_nick)
- return the_input.auto_completion(userlist, '')
- elif n == 3:
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
possible_roles = ['none', 'visitor', 'participant', 'moderator']
- return the_input.auto_completion(possible_roles, '')
+ return the_input.new_completion(possible_roles, 2, '', quotify=True)
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:
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
userlist = [user.nick for user in self.users]
if self.own_nick in userlist:
userlist.remove(self.own_nick)
@@ -892,10 +889,10 @@ class MucTab(ChatTab):
if self.core.xmpp.boundjid.bare in jidlist:
jidlist.remove(self.core.xmpp.boundjid.bare)
userlist.extend(jidlist)
- return the_input.auto_completion(userlist, '')
- elif n == 3:
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
possible_affiliations = ['none', 'member', 'admin', 'owner', 'outcast']
- return the_input.auto_completion(possible_affiliations, '')
+ return the_input.new_completion(possible_affiliations, 2, '', quotify=True)
def scroll_user_list_up(self):
self.user_win.scroll_up()
@@ -1130,15 +1127,16 @@ class MucTab(ChatTab):
self.input.refresh()
def completion_topic(self, the_input):
- current_topic = self.topic
- return the_input.auto_completion([current_topic], '', quotify=False)
+ if the_input.get_argument_position() == 1:
+ return the_input.auto_completion([self.topic], '', quotify=False)
def completion_quoted(self, the_input):
"""Nick completion, but with quotes"""
- compare_users = lambda x: x.last_talked
- word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
- if user.nick != self.own_nick]
- return the_input.auto_completion(word_list, '', quotify=True)
+ if the_input.get_argument_position(quoted=True) == 1:
+ compare_users = lambda x: x.last_talked
+ word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
+ if user.nick != self.own_nick]
+ return the_input.new_completion(word_list, 1, quotify=True)
def command_kick(self, arg):
"""
@@ -1290,7 +1288,8 @@ class MucTab(ChatTab):
self.core.information(_('%s is now unignored') % nick)
def completion_unignore(self, the_input):
- return the_input.auto_completion([user.nick for user in self.ignores], '', quotify=False)
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion([user.nick for user in self.ignores], 1, '', quotify=False)
def resize(self):
"""
@@ -2270,8 +2269,9 @@ class RosterInfoTab(Tab):
"""
Completion for /block
"""
- jids = roster.jids()
- return the_input.auto_completion(jids, '', quotify=False)
+ if the_input.get_argument_position() == 1:
+ jids = roster.jids()
+ return the_input.new_completion(jids, 1, '', quotify=False)
def command_unblock(self, arg):
"""
@@ -2296,15 +2296,16 @@ class RosterInfoTab(Tab):
"""
Completion for /unblock
"""
- try:
- iq = self.core.xmpp.plugin['xep_0191'].get_blocked(block=True)
- except Exception as e:
- iq = e.iq
- finally:
- if iq['type'] == 'error':
- return
- l = [str(item) for item in iq['blocklist']['items']]
- return the_input.auto_completion(l, quotify=False)
+ if the_input.get_argument_position():
+ try:
+ iq = self.core.xmpp.plugin['xep_0191'].get_blocked(block=True)
+ except Exception as e:
+ iq = e.iq
+ finally:
+ if iq['type'] == 'error':
+ return
+ l = sorted(str(item) for item in iq['blocklist']['items'])
+ return the_input.new_completion(l, 1, quotify=False)
def command_list_blocks(self, arg=None):
"""
@@ -2673,73 +2674,58 @@ class RosterInfoTab(Tab):
return the_input.auto_completion(jids, '', quotify=False)
def completion_name(self, the_input):
- text = the_input.get_text()
- n = len(common.shell_split(text))
- if text.endswith(' '):
- n += 1
-
- if n == 2:
+ """Completion for /name"""
+ n = the_input.get_argument_position()
+ if n == 1:
jids = [jid for jid in roster.jids()]
- return the_input.auto_completion(jids, '')
+ return the_input.new_completion(jids, n, quotify=True)
return False
def completion_groupadd(self, the_input):
- text = the_input.get_text()
- n = len(common.shell_split(text))
- if text.endswith(' '):
- n += 1
-
- if n == 2:
- jids = [jid for jid in roster.jids()]
- return the_input.auto_completion(jids, '')
- elif n == 3:
- groups = [group for group in roster.groups if group != 'none']
- return the_input.auto_completion(groups, '')
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
+ groups = sorted(group for group in roster.groups if group != 'none')
+ return the_input.new_completion(groups, n, '', quotify=True)
return False
def completion_groupmove(self, the_input):
- text = the_input.get_text()
- args = common.shell_split(text)
- n = len(args)
- if text.endswith(' '):
- n += 1
-
- if n == 2:
- jids = [jid for jid in roster.jids()]
- return the_input.auto_completion(jids, '')
- elif n == 3:
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
contact = roster[args[1]]
if not contact:
return False
groups = list(contact.groups)
if 'none' in groups:
groups.remove('none')
- return the_input.auto_completion(groups, '')
- elif n == 4:
- groups = [group for group in roster.groups]
- return the_input.auto_completion(groups, '')
+ return the_input.new_completion(groups, n, '', quotify=True)
+ elif n == 3:
+ groups = sorted(group for group in roster.groups)
+ return the_input.new_completion(groups, n, '', quotify=True)
return False
def completion_groupremove(self, the_input):
- text = the_input.get_text()
- args = common.shell_split(text)
- n = len(args)
- if text.endswith(' '):
- n += 1
-
- if n == 2:
- jids = [jid for jid in roster.jids()]
- return the_input.auto_completion(jids, '')
- elif n == 3:
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
contact = roster[args[1]]
if contact is None:
return False
- groups = list(contact.groups)
+ groups = sorted(contact.groups)
try:
groups.remove('none')
except ValueError:
pass
- return the_input.auto_completion(groups, '')
+ return the_input.new_completion(groups, n, '', quotify=True)
return False
def completion_deny(self, the_input):
@@ -2747,9 +2733,9 @@ class RosterInfoTab(Tab):
Complete the first argument from the list of the
contact with ask=='subscribe'
"""
- jids = [str(contact.bare_jid) for contact in roster.contacts.values()\
- if contact.pending_in]
- return the_input.auto_completion(jids, '', quotify=False)
+ jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values()
+ if contact.pending_in)
+ return the_input.new_completion(jids, 1, '', quotify=False)
def command_accept(self, arg):
"""
@@ -2772,6 +2758,7 @@ class RosterInfoTab(Tab):
contact = roster[jid]
if contact is None:
return
+ contact.pending_in = False
roster.modified()
self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
self.core.xmpp.client_roster.send_last_presence()
diff --git a/src/windows.py b/src/windows.py
index 6ee9fbfb..b35ad4ba 100644
--- a/src/windows.py
+++ b/src/windows.py
@@ -1383,6 +1383,122 @@ class Input(Win):
self.normal_completion(word_list, add_after)
return True
+ def new_completion(self, word_list, argument_position=-1, add_after='', quotify=True):
+ """
+ Complete the argument at position ``argument_postion`` in the input.
+ If ``quotify`` is ``True``, then the completion will operate on block of words
+ (e.g. "toto titi") whereas if it is ``False``, it will operate on words (e.g
+ "toto", "titi").
+
+ The completions may modify other parts of the input when completing an argument,
+ for example removing useless double quotes around single-words, or setting the
+ space between each argument to only one space.
+
+ The case where we complete the first argument is special, because we complete
+ the command, and we do not want to modify anything else in the input.
+
+ This method is the one that should be used if the command being completed
+ has several arguments.
+ """
+ if argument_position == 0:
+ self._new_completion_first(word_list)
+ else:
+ self._new_completion_args(word_list, argument_position, add_after, quotify)
+ self.rewrite_text()
+ return True
+
+ def _new_completion_args(self, word_list, argument_position=-1, add_after='', quoted=True):
+ """
+ Case for completing arguments with position ≠ 0
+ """
+ if quoted:
+ words = common.shell_split(self.text)
+ else:
+ words = self.text.split()
+ if argument_position >= len(words):
+ current = ''
+ else:
+ current = words[argument_position]
+
+ if quoted:
+ split_words = words[1:]
+ words = [words[0]]
+ for word in split_words:
+ if ' ' in word:
+ words.append('"' + word + '"')
+ else:
+ words.append(word)
+ current_l = current.lower()
+ if self.last_completion is not None:
+ self.hit_list.append(self.hit_list.pop(0))
+ else:
+ hit_list = []
+ for word in word_list:
+ if word.lower().startswith(current_l):
+ hit_list.append(word)
+
+ if not hit_list:
+ return
+ self.hit_list = hit_list
+
+ if argument_position >= len(words):
+ if quoted and ' ' in self.hit_list[0]:
+ words.append('"'+self.hit_list[0]+'"')
+ else:
+ words.append(self.hit_list[0])
+ else:
+ if quoted and ' ' in self.hit_list[0]:
+ words[argument_position] = '"'+self.hit_list[0]+'"'
+ else:
+ words[argument_position] = self.hit_list[0]
+
+ new_pos = -1
+ for i, word in enumerate(words):
+ if argument_position >= i:
+ new_pos += len(word) + 1
+
+ self.last_completion = self.hit_list[0]
+ self.text = words[0] + ' ' + ' '.join(words[1:])
+ self.pos = new_pos
+
+ def _new_completion_first(self, word_list):
+ """
+ Special case of completing the command itself:
+ we don’t want to change anything to the input doing that
+ """
+ space_pos = self.text.find(' ')
+ if space_pos != -1:
+ current, follow = self.text[:space_pos], self.text[space_pos:]
+ else:
+ current, follow = self.text, ''
+
+ if self.last_completion:
+ self.hit_list.append(self.hit_list.pop(0))
+ else:
+ hit_list = []
+ for word in word_list:
+ if word.lower().startswith(current):
+ hit_list.append(word)
+ if not hit_list:
+ return
+ self.hit_list = hit_list
+
+ self.last_completion = self.hit_list[0]
+ self.text = self.hit_list[0] + follow
+ self.pos = len(self.hit_list[0])
+
+ def get_argument_position(self, quoted=True):
+ """
+ Get the argument number at the current position
+ """
+ command_stop = self.text.find(' ')
+ if command_stop == -1 or self.pos <= command_stop:
+ return 0
+ text = self.text[command_stop+1:]
+ pos = self.pos + self.line_pos - len(self.text) + len(text) - 1
+ val = common.find_argument(pos, text, quoted=quoted) + 1
+ return val
+
def reset_completion(self):
"""
Reset the completion list (called on ALL keys except tab)