Merge branch 'master' into plugins

This commit is contained in:
Florent Le Coz 2011-10-29 07:20:10 +02:00
commit 0451127ff8
14 changed files with 353 additions and 62 deletions

52
README
View file

@ -48,7 +48,7 @@ you can now simply launch `poezio'
You can edit the config file (~/.config/poezio/poezio.cfg by default)
or data/default_config.cfg (if you want to edit the config before the
first launch). The default config file is fully commented.
first launch). The default config file is fully commented.
Please, see the online documentation for more information on installing,
configuring or using poezio:
@ -66,13 +66,15 @@ feature you want.
Florent Le Coz (louiz) <louiz@louiz.org> (main developper)
Mathieu Pasquet (mathieui) <mathieui@mathieui.net> (developper)
=======================
Contact/support
=======================
Jabber ChatRoom: poezio@kikoo.louiz.org
Jabber ChatRoom: poezio@muc.poezio.eu
Forum: http://dev.louiz.org/project/poezio/forum
Report a bug: http://dev.louiz.org/project/poezio/bugs/add
=======================
License
=======================
@ -85,6 +87,52 @@ Please read the COPYING file for details.
The artwork logo was made by Gaëtan Ribémont and released under
the Creative Commons BY license (http://creativecommons.org/licenses/by/2.0/)
=======================
Hacking
=======================
If you want to contribute, you are invited on poezio@muc.poezio.eu to
announce your ideas, what you are going to do, or to seek help if you
have trouble understanding some of the code.
The preferred way to submit changes is through a public git repository.
But mercurial repositories or simple patches are also welcome.
For contributors having commit access:
This section explains how the git repository is organized.
The “master” branch is the branch where all recent development is made. This is
the unstable version, which can be broken, but we should try to keep it usable
and crash-free as much as possible (so, never push to it if you are adding a
*known* crash).
New big features that take time to be complete should be developped in feature
branches (for example the “plugins” or the “opt” branches).
If its a really long feature, merge the “master” branch in that feature branch
from time to time, to avoid huge merges (and merge issues) when youll have to
merge your feature back in “master”.
Merge your work in master once it works and is usable, not necessarily when
its 100% finished. Polishing and last bug fixes can take place in “master”.
Conflicts should be solved with *rebase* and not with merge. This means
that if two developpers commited one thing at the same time in their own
repository, the first pushes on the public public repos, and the other
has to pull before being able to push too. In that case, the second
developper should use the rebase command instead of merge. This avoids
creating unnecessary “branches” and visible merges.
On the contrary, when merging feature branches back to “master”, we should
use merge with the --no-ff tag (this makes sure the branch will always
distinctly appear in the logs), even if no conflict occured.
Finally, when a release is ready, we should merge the “master” branch
into the releases branch, then tag it to that version number.
If an “urgent” bugfix has to be made for a release (for example
a security issue is discovered on the last stable version, and
the current master has evolved too much to be released in the current
state), we create a new bugfix branch from the “releases” branch, we fix
it and finally merge it back to the “releases” branch, and tag it (and
we merge it to “master” as well, of course).
=======================
Thanks
=======================

View file

@ -34,7 +34,7 @@ password =
# the rooms you will join automatically on startup, with associated nickname or not
# format : room@server.tld/nickname:room2@server.tld/nickname2
# default_nick will be used if "/nickname" is not specified
rooms = poezio@kikoo.louiz.org
rooms = poezio@muc.poezio.eu
# the completion type you will use to complete nicknames
# if "normal", complete the entire name to the first available completion

View file

@ -10,7 +10,7 @@ setup (name = 'BuildLines',
author = 'Florent Le Coz',
author_email = 'louiz@louiz.org',
long_description = """
a python3 module for poezio, used to replace some time-critical
python functions that are too slow. If compiled, poezio will use this module,
otherwise it will just use the equivalent python functions.
""")
a python3 module for poezio, used to replace some time-critical
python functions that are too slow. If compiled, poezio will use this module,
otherwise it will just use the equivalent python functions.
""")

View file

@ -66,6 +66,12 @@ class Contact(object):
self._ask = None
self._groups = [] # a list of groups the contact is in
def get_groups(self):
"""
Return the groups the contact is in
"""
return self._groups
def get_bare_jid(self):
"""
Just get the bare_jid or the contact

View file

@ -350,7 +350,7 @@ class Core(object):
"""
When a data form is received
"""
self.information('%s' % messsage)
self.information('%s' % message)
def on_chatstate_active(self, message):
self.on_chatstate(message, "active")
@ -1105,7 +1105,7 @@ class Core(object):
nick_from = message['mucnick']
room_from = message.getMucroom()
if message['type'] == 'error': # Check if it's an error
return self.room_error(message, from_room)
return self.room_error(message, room_from)
room = self.get_room_by_name(room_from)
tab = self.get_tab_by_name(room_from, tabs.MucTab)
if tab and tab.get_room() and tab.get_room().get_user_by_name(nick_from) and\
@ -1360,7 +1360,7 @@ class Core(object):
"""
/join [room][/nick] [password]
"""
args = arg.split()
args = common.shell_split(arg)
password = None
if len(args) == 0:
t = self.current_tab()

View file

@ -428,7 +428,7 @@ class FormWin(object):
self._win = curses.newwin(height, width, y, x)
self.current_input = 0
self.inputs = [] # dict list
for (name, field) in self._form.getFields():
for (name, field) in self._form.getFields().items():
if field['type'] == 'hidden':
continue
try:
@ -508,7 +508,7 @@ class FormWin(object):
self._win.erase()
y = 0
i = 0
for name, field in self._form.getFields():
for name, field in self._form.getFields().items():
if field['type'] == 'hidden':
continue
self.inputs[i]['label'].resize(1, self.width//3, y + 1, 0)

View file

@ -67,13 +67,14 @@ def leave_groupchat(xmpp, jid, own_nick, msg):
"""
xmpp.plugin['xep_0045'].leaveMUC(jid, own_nick, msg)
def eject_user(xmpp, jid, nick, reason):
def set_user_role(xmpp, jid, nick, reason, role):
"""
(try to) Eject an user from the room
(try to) Set the role of a MUC user
(role = 'none': eject user)
"""
iq = xmpp.makeIqSet()
query = ET.Element('{%s}query' % NS_MUC_ADMIN)
item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'role':'none'})
item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'role':role})
if reason:
reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN)
reason_el.text = reason
@ -81,4 +82,26 @@ def eject_user(xmpp, jid, nick, reason):
query.append(item)
iq.append(query)
iq['to'] = jid
return iq.send()
try:
return iq.send()
except Exception as e:
return e.iq
def set_user_affiliation(xmpp, jid, nick, reason, affiliation):
"""
(try to) Set the affiliation of a MUC user
"""
iq = xmpp.makeIqSet()
query = ET.Element('{%s}query' % NS_MUC_ADMIN)
item = ET.Element('{%s}item' % NS_MUC_ADMIN, {'nick':nick, 'affiliation':affiliation})
if reason:
reason_el = ET.Element('{%s}reason' % NS_MUC_ADMIN)
reason_el.text = reason
item.append(reason_el)
query.append(item)
iq.append(query)
iq['to'] = jid
try:
return iq.send()
except Exception as e:
return e.iq

View file

@ -27,6 +27,8 @@ def main():
signal.signal(signal.SIGINT, signal.SIG_IGN) # ignore ctrl-c
if options.debug:
logging.basicConfig(filename=options.debug, level=logging.DEBUG)
else:
logging.basicConfig(level=logging.CRITICAL)
cocore = singleton.Singleton(core.Core)
cocore.start()
if not cocore.xmpp.start(): # Connect to remote server

View file

@ -90,6 +90,7 @@ class Roster(object):
for group in contact._groups:
if group not in groups:
# the contact is not in the group anymore
contact._groups.remove(group)
self.remove_contact_from_group(group, contact)
def remove_contact_from_group(self, group_name, contact):

View file

@ -126,6 +126,12 @@ class Tab(object):
words = ['/%s'% (name) for name in self.core.commands] +\
['/%s' % (name) for name in self.commands]
the_input.auto_completion(words, '')
# 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.
if len(the_input.hit_list) == 1:
the_input.do_command(' ')
return True
return False
@ -283,7 +289,7 @@ class ChatTab(Tab):
for msg in self._room.messages[:-40:-1]:
if not msg:
continue
txt = msg.txt
txt = xhtml.clean_text(msg.txt)
for char in char_we_dont_want:
txt = txt.replace(char, ' ')
for word in txt.split():
@ -294,7 +300,7 @@ class ChatTab(Tab):
def on_enter(self):
txt = self.input.key_enter()
if txt:
clean_text = xhtml.clean_text(txt)
clean_text = xhtml.clean_text_simple(txt)
if not self.execute_command(clean_text):
if txt.startswith('//'):
txt = txt[1:]
@ -402,6 +408,8 @@ class MucTab(ChatTab):
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."), 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: outcast, none, member, admin, owner. You also can give an optional reason."), None)
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['part'] = (self.command_part, _("Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message."), None)
@ -412,6 +420,8 @@ class MucTab(ChatTab):
self.commands['configure'] = (self.command_configure, _('Usage: /configure\nConfigure: Configure the current room, through a form.'), None)
self.commands['version'] = (self.command_version, _('Usage: /version <jid or nick>\nVersion: get the software version of the given JID or nick in room (usually its XMPP client and Operating System)'), 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: clears the current buffer'"""), None)
self.resize()
def scroll_user_list_up(self):
@ -459,12 +469,21 @@ 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._room.messages = []
self.text_win.rebuild_everything(self._room)
self.refresh()
self.core.doupdate()
def command_cycle(self, arg):
if self.get_room().joined:
muc.leave_groupchat(self.core.xmpp, self.get_name(), self.get_room().own_nick, arg)
self.get_room().disconnect()
self.core.disable_private_tabs(self.get_room().name)
self.core.command_join('/%s' % self.core.get_bookmark_nickname(self.get_room().name), '0')
self.core.command_join('"/%s"' % self.core.get_bookmark_nickname(self.get_room().name), '0')
self.user_win.pos = 0
def command_recolor(self, arg):
@ -604,24 +623,60 @@ class MucTab(ChatTab):
def completion_topic(self, the_input):
current_topic = self.get_room().topic
return the_input.auto_completion([current_topic], ' ')
return the_input.auto_completion([current_topic], '')
def command_kick(self, arg):
"""
/kick <nick> [reason]
"""
args = common.shell_split(arg)
if len(args) < 1:
if not len(args):
self.core.command_help('kick')
self._command_change_role('kick '+arg)
def command_role(self, arg):
"""
/role <nick> <role> [reason]
Changes the role of an user
roles can be: none, visitor, participant, moderator
"""
args = common.shell_split(arg)
if len(args) < 2:
self.core.command_help('role')
return
nick = args[0]
if len(args) >= 2:
reason = ' '.join(args[1:])
nick, role = args[0],args[1]
if len(args) > 2:
reason = ' '.join(args[2:])
else:
reason = ''
if not self.get_room().joined:
if not self.get_room().joined or \
not role in ('none', 'visitor', 'participant', 'moderator'):
return
res = muc.eject_user(self.core.xmpp, self.get_name(), nick, reason)
res = muc.set_user_role(self.core.xmpp, self.get_name(), nick, reason, role)
if res['type'] == 'error':
self.core.room_error(res, self.get_name())
def command_affiliation(self, arg):
"""
/affiliation <nick> <role> [reason]
Changes the affiliation of an user
roles can be: none, visitor, participant, moderator
"""
args = common.shell_split(arg)
if len(args) < 2:
self.core.command_help('role')
return
nick, affiliation = args[0],args[1]
if len(args) > 2:
reason = ' '.join(args[2:])
else:
reason = ''
if not self.get_room().joined or \
not affiliation in ('none', 'member', 'admin', 'owner'):
# replace this ↑ with this ↓ when the ban list support is done
# not affiliation in ('outcast', 'none', 'member', 'admin', 'owner'):
return
res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), nick, reason, affiliation)
if res['type'] == 'error':
self.core.room_error(res, self.get_name())
@ -632,7 +687,7 @@ class MucTab(ChatTab):
if line.find('\x19') == -1:
msg['body'] = line
else:
msg['body'] = xhtml.clean_text(line)
msg['body'] = xhtml.clean_text_simple(line)
msg['xhtml_im'] = xhtml.poezio_colors_to_html(line)
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False:
msg['chat_state'] = needed
@ -729,12 +784,12 @@ class MucTab(ChatTab):
word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)\
if user.nick != self._room.own_nick]
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):
input_pos = self.input.pos + self.input.line_pos
if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\
self.input.get_text()[:input_pos] == self.input.last_completion + after):
add_after = after
else:
add_after = ' '
self.input.auto_completion(word_list, add_after)
empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
self.send_composing_chat_state(empty_after)
@ -1024,7 +1079,7 @@ class PrivateTab(ChatTab):
if line.find('\x19') == -1:
msg['body'] = line
else:
msg['body'] = xhtml.clean_text(line)
msg['body'] = xhtml.clean_text_simple(line)
msg['xhtml_im'] = xhtml.poezio_colors_to_html(line)
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False:
needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active'
@ -1215,6 +1270,9 @@ class RosterInfoTab(Tab):
self.commands['deny'] = (self.command_deny, _("Usage: /deny [jid]\nDeny: Use this command to remove and 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: Use this command to authorize the provided JID (or the selected contact in your roster), to see your presence, and to ask to subscribe to it (mutual presence subscription)."), self.completion_deny)
self.commands['add'] = (self.command_add, _("Usage: /add <jid>\nAdd: Use this command to add the specified JID to your roster. The reverse authorization will automatically be accepted if the remote JID accepts your subscription, leading to a mutual presence subscription."), 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['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: Use this command to 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: Use this command to 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: Use this command to import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not."), None)
@ -1267,6 +1325,87 @@ class RosterInfoTab(Tab):
return
self.core.xmpp.sendPresence(pto=jid, ptype='subscribe')
def command_name(self, args):
"""
Set a name for the specified JID in your roster
"""
args = args.split(None, 1)
if len(args) < 1:
return
jid = JID(args[0]).bare
name = args[1] if len(args) == 2 else ''
contact = roster.get_contact_by_jid(jid)
if not contact:
self.core.information(_('No such JID in roster'), 'Error')
return
groups = set(contact.get_groups())
subscription = contact.get_subscription()
if self.core.xmpp.update_roster(jid, name=name, groups=groups, subscription=subscription):
contact.set_name(name)
def command_groupadd(self, args):
"""
Add the specified JID to the specified group
"""
args = args.split(None, 1)
if len(args) != 2:
return
jid = JID(args[0]).bare
group = args[1]
contact = roster.get_contact_by_jid(jid)
if not contact:
self.core.information(_('No such JID in roster'), 'Error')
return
new_groups = set(contact.get_groups())
if group in new_groups:
self.core.information(_('JID already in group'), 'Error')
return
new_groups.add(group)
try:
new_groups.remove('none')
except KeyError:
pass
name = contact.get_name()
subscription = contact.get_subscription()
if self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription):
roster.edit_groups_of_contact(contact, new_groups)
def command_groupremove(self, args):
"""
Remove the specified JID to the specified group
"""
args = args.split(None, 1)
if len(args) != 2:
return
jid = JID(args[0]).bare
group = args[1]
contact = roster.get_contact_by_jid(jid)
if not contact:
self.core.information(_('No such JID in roster'), 'Error')
return
new_groups = set(contact.get_groups())
try:
new_groups.remove('none')
except KeyError:
pass
if group not in new_groups:
self.core.information(_('JID not in group'), 'Error')
return
new_groups.remove(group)
name = contact.get_name()
subscription = contact.get_subscription()
if self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription):
roster.edit_groups_of_contact(contact, new_groups)
def command_remove(self, args):
"""
Remove the specified JID from the roster. i.e. : unsubscribe
@ -1342,6 +1481,53 @@ class RosterInfoTab(Tab):
jids = [contact.get_bare_jid() for contact in roster.get_contacts()]
return the_input.auto_completion(jids, '')
def completion_name(self, the_input):
text = the_input.get_text()
n = len(text.split())
if text.endswith(' '):
n += 1
if n == 2:
jids = [contact.get_bare_jid() for contact in roster.get_contacts()]
return the_input.auto_completion(jids, '')
return False
def completion_groupadd(self, the_input):
text = the_input.get_text()
n = len(text.split())
if text.endswith(' '):
n += 1
if n == 2:
jids = [contact.get_bare_jid() for contact in roster.get_contacts()]
return the_input.auto_completion(jids, '')
elif n == 3:
groups = [group.name for group in roster.get_groups() if group.name != 'none']
return the_input.auto_completion(groups, '')
return False
def completion_groupremove(self, the_input):
text = the_input.get_text()
args = text.split()
n = len(args)
if text.endswith(' '):
n += 1
if n == 2:
jids = [contact.get_bare_jid() for contact in roster.get_contacts()]
return the_input.auto_completion(jids, '')
elif n == 3:
contact = roster.get_contact_by_jid(args[1])
if not contact:
return False
groups = list(contact.get_groups())
try:
groups.remove('none')
except ValueError:
pass
return the_input.auto_completion(groups, '')
return False
def completion_deny(self, the_input):
"""
Complete the first argument from the list of the
@ -1421,7 +1607,8 @@ class RosterInfoTab(Tab):
self.input.do_command("/") # we add the slash
def reset_help_message(self, _=None):
curses.curs_set(0)
if self.core.current_tab() is self:
curses.curs_set(0)
self.input = self.default_help_message
self.input.refresh()
self.core.doupdate()
@ -1563,7 +1750,7 @@ class ConversationTab(ChatTab):
if line.find('\x19') == -1:
msg['body'] = line
else:
msg['body'] = xhtml.clean_text(line)
msg['body'] = xhtml.clean_text_simple(line)
msg['xhtml_im'] = xhtml.poezio_colors_to_html(line)
if config.get('send_chat_states', 'true') == 'true' and self.remote_wants_chatstates is not False:
needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active'

View file

@ -118,7 +118,7 @@ class Theme(object):
# A list of colors randomly attributed to nicks in MUCs
# Setting more colors makes it harder to have two nicks with the same color,
# avoiding confusions.
LIST_COLOR_NICKNAMES = [(1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (7, -1), (8, -1), (9, -1), (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (23, -1), (23, -1), (88, -1), (99, -1), (100, -1), (154, -1), (213, -1), (216, -1), (227, -1)]
LIST_COLOR_NICKNAMES = [(1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (8, -1), (9, -1), (10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (23, -1), (23, -1), (88, -1), (99, -1), (100, -1), (154, -1), (213, -1), (216, -1), (227, -1)]
# This is your own nickname
COLOR_OWN_NICK = (254, -1)

View file

@ -122,7 +122,6 @@ class Win(object):
self.move(y, x)
next_attr_char = text.find('\x19')
while next_attr_char != -1 and text:
log.debug('Addstr_Colored: [%s]' % text.replace('\x19', '\\x19'))
if next_attr_char + 1 < len(text):
attr_char = text[next_attr_char+1].lower()
else:
@ -883,26 +882,28 @@ class Input(Win):
self.rewrite_text()
return True
def key_left(self, jump=True):
def key_left(self, jump=True, reset=True):
"""
Move the cursor one char to the left
"""
self.reset_completion()
if reset:
self.reset_completion()
if self.pos == self.width-1 and self.line_pos > 0:
self.line_pos -= 1
elif self.pos >= 1:
self.pos -= 1
if jump and self.pos+self.line_pos >= 1 and self.text[self.pos+self.line_pos-1] == '\x19':
self.key_left()
else:
elif reset:
self.rewrite_text()
return True
def key_right(self, jump=True):
def key_right(self, jump=True, reset=True):
"""
Move the cursor one char to the right
"""
self.reset_completion()
if reset:
self.reset_completion()
if self.pos == self.width-1:
if self.line_pos + self.width-1 < len(self.text):
self.line_pos += 1
@ -910,7 +911,7 @@ class Input(Win):
self.pos += 1
if jump and self.pos+self.line_pos < len(self.text) and self.text[self.pos+self.line_pos-1] == '\x19':
self.key_right()
else:
elif reset:
self.rewrite_text()
return True
@ -936,8 +937,6 @@ class Input(Win):
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(word_list, add_after)
@ -957,33 +956,42 @@ class Input(Win):
Normal completion
"""
(y, x) = self._win.getyx()
pos = self.pos + self.line_pos
if pos < len(self.text) and after.endswith(' ') and self.text[pos] == ' ':
after = after[:-1] # remove the last space if we are already on a space
if not self.last_completion:
# begin is the begining of the word we want to complete
if self.text.strip() and not self.text.endswith(' '):
begin = self.text.split()[-1].lower()
space_before_cursor = self.text.rfind(' ', 0, pos)
if space_before_cursor != -1:
begin = self.text[space_before_cursor+1:pos]
else:
begin = ''
begin = self.text[:pos]
hit_list = [] # list of matching nicks
for word in word_list:
if word.lower().startswith(begin):
if word.lower().startswith(begin.lower()):
hit_list.append(word)
if len(hit_list) == 0:
return
self.hit_list = hit_list
end = len(begin)
else:
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
begin = self.last_completion
end = len(begin) + len(after)
if end:
self.text = self.text[:-end]
self.hit_list.append(self.hit_list.pop(0)) # rotate list
self.text = self.text[:pos-end] + self.text[pos:]
pos -= end
nick = self.hit_list[0] # take the first hit
self.text = self.text[:pos] + nick + after + self.text[pos:]
for i in range(end):
try:
self.key_left(reset=False)
except:
pass
for i in range(len(nick + after)):
self.key_right(reset=False)
self.rewrite_text()
self.last_completion = nick
self.text += nick +after
self.key_end(False)
def shell_completion(self, word_list, after):
"""
@ -1034,7 +1042,8 @@ class Input(Win):
return res
if not key or len(key) > 1:
return False # ignore non-handled keyboard shortcuts
self.reset_completion()
if reset:
self.reset_completion()
self.text = self.text[:self.pos+self.line_pos]+key+self.text[self.pos+self.line_pos:]
(y, x) = self._win.getyx()
if x == self.width-1:

View file

@ -176,6 +176,8 @@ log = logging.getLogger(__name__)
whitespace_re = re.compile(r'\s+')
xhtml_attr_re = re.compile(r'\x19\d{0,3}\}|\x19[buaio]')
def get_body_from_message_stanza(message):
"""
Returns a string with xhtml markups converted to
@ -249,7 +251,11 @@ def xhtml_to_poezio_colors(text):
log.debug(text)
xml = ET.fromstring(text)
message = ''
for elem in xml.iter():
if version_info[1] == 2:
elems = xml.iter()
else:
elems = xml.getiterator()
for elem in elems:
if elem.tag == '{http://www.w3.org/1999/xhtml}a':
if 'href' in elem.attrib and elem.attrib['href'] != elem.text:
message += '\x19u%s\x19o (%s)' % (trim(elem.attrib['href']), trim(elem.text))
@ -317,9 +323,18 @@ def xhtml_to_poezio_colors(text):
return message
def clean_text(string):
def clean_text(s):
"""
Remove all \x19 from the string
Remove all xhtml-im attributes (\x19etc) from the string with the
complete color format, i.e \x19xxx}
"""
s = re.sub(xhtml_attr_re, "", s)
return s
def clean_text_simple(string):
"""
Remove all \x19 from the string formatted with simple colors:
\x198
"""
pos = string.find('\x19')
while pos != -1:

View file

@ -7,7 +7,7 @@
# Use launch.sh to start poezio directly from here
echo 'Updating poezio'
hg pull -u
git pull origin master
if [ -e "SleekXMPP" ]
then