summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README52
-rw-r--r--data/default_config.cfg2
-rw-r--r--setup.py8
-rw-r--r--src/contact.py6
-rw-r--r--src/core.py6
-rw-r--r--src/data_forms.py4
-rw-r--r--src/multiuserchat.py31
-rw-r--r--src/poezio.py2
-rw-r--r--src/roster.py1
-rw-r--r--src/tabs.py221
-rw-r--r--src/theming.py2
-rw-r--r--src/windows.py57
-rw-r--r--src/xhtml.py21
-rwxr-xr-xupdate.sh2
14 files changed, 353 insertions, 62 deletions
diff --git a/README b/README
index 14257789..dbce3f79 100644
--- a/README
+++ b/README
@@ -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 it’s a really long feature, merge the “master” branch in that feature branch
+from time to time, to avoid huge merges (and merge issues) when you’ll have to
+merge your feature back in “master”.
+Merge your work in master once it works and is usable, not necessarily when
+it’s 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
=======================
diff --git a/data/default_config.cfg b/data/default_config.cfg
index b061cbf4..858c90bf 100644
--- a/data/default_config.cfg
+++ b/data/default_config.cfg
@@ -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
diff --git a/setup.py b/setup.py
index 496d3d7b..31ccc5a6 100644
--- a/setup.py
+++ b/setup.py
@@ -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.
+ """)
diff --git a/src/contact.py b/src/contact.py
index 9d2885be..99c24a32 100644
--- a/src/contact.py
+++ b/src/contact.py
@@ -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
diff --git a/src/core.py b/src/core.py
index b39bf904..1fb06b38 100644
--- a/src/core.py
+++ b/src/core.py
@@ -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()
diff --git a/src/data_forms.py b/src/data_forms.py
index 99d08caa..8445d3d2 100644
--- a/src/data_forms.py
+++ b/src/data_forms.py
@@ -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)
diff --git a/src/multiuserchat.py b/src/multiuserchat.py
index 10ea0daf..264f0e4a 100644
--- a/src/multiuserchat.py
+++ b/src/multiuserchat.py
@@ -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
diff --git a/src/poezio.py b/src/poezio.py
index dc877626..7b55ce96 100644
--- a/src/poezio.py
+++ b/src/poezio.py
@@ -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
diff --git a/src/roster.py b/src/roster.py
index fe68584b..4ef2da1b 100644
--- a/src/roster.py
+++ b/src/roster.py
@@ -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):
diff --git a/src/tabs.py b/src/tabs.py
index 8606c450..a160a0a1 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -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'
diff --git a/src/theming.py b/src/theming.py
index 382a3146..4bfdad42 100644
--- a/src/theming.py
+++ b/src/theming.py
@@ -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)
diff --git a/src/windows.py b/src/windows.py
index 2352a82a..4f2c68c6 100644
--- a/src/windows.py
+++ b/src/windows.py
@@ -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:
diff --git a/src/xhtml.py b/src/xhtml.py
index 9bb2705d..38239d18 100644
--- a/src/xhtml.py
+++ b/src/xhtml.py
@@ -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 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
+ Remove all \x19 from the string formatted with simple colors:
+ \x198
"""
pos = string.find('\x19')
while pos != -1:
diff --git a/update.sh b/update.sh
index cc7b8962..510bbb07 100755
--- a/update.sh
+++ b/update.sh
@@ -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