summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--README2
-rw-r--r--data/default_config.cfg7
-rw-r--r--data/themes/dark.py1
-rw-r--r--doc/en/install.txt40
-rw-r--r--doc/en/keys.txt11
-rw-r--r--doc/en/plugins.txt132
-rw-r--r--doc/en/usage.txt310
-rw-r--r--doc/images/conversation.pngbin0 -> 12992 bytes
-rw-r--r--doc/images/data_forms.pngbin0 -> 8624 bytes
-rw-r--r--doc/images/list.pngbin0 -> 7768 bytes
-rw-r--r--doc/images/muc.pngbin0 -> 42090 bytes
-rw-r--r--doc/images/private.pngbin0 -> 9145 bytes
-rw-r--r--doc/images/roster.pngbin0 -> 14736 bytes
-rw-r--r--doc/images/tab_bar.pngbin0 -> 956 bytes
-rw-r--r--plugins/alias.py40
-rw-r--r--plugins/day_change.py30
-rw-r--r--plugins/exec.py43
-rw-r--r--plugins/figlet.py13
-rw-r--r--plugins/link.py47
-rw-r--r--plugins/mpd_client.py29
-rw-r--r--plugins/rainbow.py20
-rw-r--r--plugins/screen_detach.py46
-rw-r--r--plugins/status.py21
-rw-r--r--plugins/test.py20
-rw-r--r--plugins/translate.py34
-rw-r--r--src/core.py148
-rwxr-xr-xsrc/daemon.py50
-rw-r--r--src/data_forms.py21
-rw-r--r--src/events.py70
-rw-r--r--src/fifo.py70
-rw-r--r--src/plugin.py92
-rw-r--r--src/plugin_manager.py137
-rw-r--r--src/tabs.py96
-rw-r--r--src/windows.py38
-rw-r--r--src/xhtml.py2
36 files changed, 1461 insertions, 111 deletions
diff --git a/Makefile b/Makefile
index 1fd424ad..29ef9756 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ clean:
find ./ -name "#*#" -delete
find ./ -name "*.html" -delete
-install:
+install: all
mkdir -p $(DESTDIR)$(prefix)
install -d $(DESTDIR)$(LOCALEDIR) $(DESTDIR)$(BINDIR) $(DESTDIR)$(DATADIR)/poezio $(DESTDIR)$(DATADIR)/poezio/data $(DESTDIR)$(DATADIR)/poezio/src/ $(DESTDIR)$(DATADIR)/poezio/src $(DESTDIR)$(DATADIR)/poezio/data/themes $(DESTDIR)$(MANDIR)/man1
diff --git a/README b/README
index 9897e687..752693d3 100644
--- a/README
+++ b/README
@@ -143,6 +143,7 @@ we merge it to “master” as well, of course).
Thanks
=======================
= People =
+ - Todd Eisenberger (todd@teisen.be) - Plugin system
- Link Mauve - Code, testing
- Gaëtan Ribémont (http://www.bonbref.com) - Logo design
- Ovart - Testing
@@ -153,3 +154,4 @@ we merge it to “master” as well, of course).
on the jabber chatroom doing bug reports and/or feature requests.
= Project =
Gajim - send_vcard method and common.py
+
diff --git a/data/default_config.cfg b/data/default_config.cfg
index 279f902d..e803a7d1 100644
--- a/data/default_config.cfg
+++ b/data/default_config.cfg
@@ -125,6 +125,13 @@ use_log = false
# you want to use instead. This directory will be created if it doesn't exist
log_dir =
+# If plugins_dir is not set, plugins will be loaded from $XDG_DATA_HOME/poezio/plugins.
+# You can specify an other directory to use. It will be created if it doesn't exist
+plugins_dir =
+
+# Space separated list of plugins to load on startup
+plugins_autoload =
+
# the full path to the photo (avatar) you want to use
# it should be less than 16Ko
# The avatar is not set by default, because it slows
diff --git a/data/themes/dark.py b/data/themes/dark.py
index bbe226f8..015e2988 100644
--- a/data/themes/dark.py
+++ b/data/themes/dark.py
@@ -8,6 +8,7 @@ class DarkTheme(theming.Theme):
COLOR_STATUS_CHAT = (34 , -1)
COLOR_STATUS_UNAVAILABLE = (242 , -1)
COLOR_STATUS_ONLINE = (27 , -1)
+ COLOR_STATUS_NONE = (27 , -1)
COLOR_VERTICAL_SEPARATOR = (236, -1)
COLOR_NEW_TEXT_SEPARATOR = (213, -1)
diff --git a/doc/en/install.txt b/doc/en/install.txt
index 75cc0ea8..4338afef 100644
--- a/doc/en/install.txt
+++ b/doc/en/install.txt
@@ -5,31 +5,44 @@ Install
Poezio in the GNU/Linux distributions
-------------------------------------
-As far as I know, Poezio is available in the following distributions, you just have to install it by using the package manager of the distribution, if you're using one of these.
-
-* *Archlinux*: A poezio and poezio-git packages are in AUR (use your favourite AUR wrapper to install them)
-* *Frugalware*: Just use pacmang-g2 to install the poezio package. (Thanks to its maintainer, Kooda)
+As far as I know, Poezio is available in the following distributions, you just
+ have to install it by using the package manager of the distribution, if you're
+ using one of these.
+
+* *Archlinux*: A poezio and poezio-git packages are in AUR (use your favourite
+ AUR wrapper to install them)
+* *Frugalware*: Just use pacmang-g2 to install the poezio package. (Thanks to
+ its maintainer, Kooda)
* *Debian*: Use an other distro.
-(If an other distribution provides a poezio package, please tell us and we will add it to the list)
+(If an other distribution provides a poezio package, please tell us and we will
+ add it to the list)
Install poezio from the sources
-------------------------------
-You can download poezio's https://dev.louiz.org/project/poezio/download[stable sources] or fetch the development version (trunk), using git:
+You can download poezio's https://dev.louiz.org/project/poezio/download[stable
+ sources] or fetch the development version (trunk), using git:
============================
git clone https://git.louiz.org/poezio
============================
-In order for poezio to correctly work, you need the libs SleekXMPP and dnspython. You can install them by downloading it from the https://github.com/fritzy/SleekXMPP/[SleekXMPP] page and the http://www.dnspython.org/[dnspython] page , but you'll need the development versions. Alternatively, you can download poezio's sources including SleekXMPP and dnspython, that's the easier way.
+In order for poezio to correctly work, you need the libs SleekXMPP and
+ dnspython. You can install them by downloading it from the
+ https://github.com/fritzy/SleekXMPP/[SleekXMPP] page and the
+ http://www.dnspython.org/[dnspython] page , but you'll need the development
+ versions. Alternatively, you can download poezio's sources including SleekXMPP
+ and dnspython, that's the easier way.
-As for dnspython, you will have to use our python3 fork, or poke them to accept patches.
+As for dnspython, you will have to use our python3 fork, or poke them to accept
+ patches.
=== Dependencies ===
-If you want to install SleekXMPP and dnspython yourself, follow these instructions. Else, go to the next section.
+If you want to install SleekXMPP and dnspython yourself, follow these
+ instructions. Else, go to the next section.
Download SleekXMPP
@@ -51,7 +64,8 @@ python3 setup.py build
su -c "python3 setup.py install"
============================
-Clone the repository at http://hg.louiz.org/dnspython (this is a fork, because upstream is unresponsive and didn’t fix an important bug).
+Clone the repository at http://hg.louiz.org/dnspython (this is a fork, because
+ upstream is unresponsive and didn’t fix an important bug).
============================
hg clone http://hg.louiz.org/dnspython
@@ -68,12 +82,14 @@ su -c "python3 setup.py install"
=== Poezio installation ===
-If you skipped the installation of the dependencies and you only want to test poezio without a system-wide install, do, in the _poezio_ directory:
+If you skipped the installation of the dependencies and you only want to test
+ poezio without a system-wide install, do, in the _poezio_ directory:
============================
sh update.sh
============================
-If you have git and hg installed, it will download and update locally the libraries for you.
+If you have git and hg installed, it will download and update locally the
+ libraries for you.
If you don't want to install poezio but just test it, do:
diff --git a/doc/en/keys.txt b/doc/en/keys.txt
index f4b659f7..109e5df5 100644
--- a/doc/en/keys.txt
+++ b/doc/en/keys.txt
@@ -92,6 +92,17 @@ These keys work only in the MultiUserChat tab.
*tabulation*:: Complete a nick.
+*Ctrl-c*:: Insert xhtml formatting. You have to press Ctrl-c then a character
+ listed below:
+- 1: Red
+- 2: Green
+- 3: Yellow/Orange
+- 4: Blue
+- 5: Pink
+- 6: Turquoise
+- b: Bold
+- o: Stop formatting
+
MultiUserChat List tab input keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt
new file mode 100644
index 00000000..47ab7f01
--- /dev/null
+++ b/doc/en/plugins.txt
@@ -0,0 +1,132 @@
+Plugins
+=======
+
+Currently, the plugins are in a plugin branch on the git repo.
+
+Location
+--------
+
+The plugins have to be present in '$XDG_DATA_HOME/poezio/plugins/plugin_name.py'
+(or '~/.local/share' if not defined)
+
+Structure
+---------
+
+A plugin must always be a class named Plugin that inherits the
+plugin.BasePlugin class defined into the *plugin* poezio module.
+
+Methods
+-------
+
+Overridden methods
+~~~~~~~~~~~~~~~~~~
+The *Plugin* class has several method that you can override for your own convenience
+
+[[init]]
+*init*:: +self+ +
+This method is called when the plugin is loaded, this is where you call
+the other methods, for example <<add-command,add_command>>, and initialize
+everything to make your plugin actually do something. <<example-1,ex 1>>, <<example-2,ex 2>>
+
+*cleanup*:: +self+ +
+Called when the plugin is unloaded (or when poezio is exited). Clean everything
+that needs to be cleaned here.
+
+Callable methods
+~~~~~~~~~~~~~~~~
+The BasePlugin has several methods that can be used. Here is a list of
+all its methods that you could use in your plugin, describing what they
+do, their arguments, and giving some example for each of them.
+
+[[add-command]]
+*add_command*:: +self+, +name+, +handler+, +help+, +completion=None+ +
+This method adds a global command to poezio. For example you can add a /foo
+command that the user could call when the plugin is loaded, by calling this
+method with _foo_ as its _name_ argument. <<example-1,ex 1>>
+
+* _name_: (string) the name of the command (for example, if it is 'plugintest', it can
+add a /plugintest command)
+* _handler_: (function) the function to be called when the command is executed.
+the handler takes *args* as a parameter, which is a string of what
+is entered after the command. Split *args* (with _common.shell_split_) to use
+that as command arguments.
+* _help_: (string) the help message available for that command through the _/help_
+command.
+* _completion_: (function) the completion for the args of that command. It takes
+an input object as its only argument. This function should call the
+_auto_completion()_ method on the input object, passing a list of possible strings
+for the completion and returning the value of that call directly.
+Everything else is handled by that _auto_completion()_ method (checking what strings
+match, how to cycle between matches, etc). If you don’t want any special completion
+for that command, just pass None (the default value).
+
+*add_event_handler**: +self+, +event_name+, +handler+ +
+This methods adds a callback that will be called whenever the given event
+occurs. <<example-2,ex 2>>
+
+* _event_name_: (string) The name of the event you want to ``monitor''.
+This can be a sleekxmpp event, or a poezio event. See the list of
+<<events-list,all available events>>.
+* _handler_: The method that will be called whenever the event occurs.
+It must accept the arguments specified for that event in the <<events-list,events list>>.
+
+Attributes
+----------
+
+Config
+~~~~~~
+By default, each plugin has a PluginConfig object accessible with *self.config*.
+
+*PluginConfig.read*:: +self+ +
+Reads the config file from $XDG_CONFIG_HOME/poezio/plugins/plugin_name.cfg, it is called upon initialization, so you should not need it.
+
+*PluginConfig.write*:: +self+ +
+Writes the config to the same file mentioned previously.
+
+Core
+~~~~
+Each plugin has a reference to the Core object accessible with *self.core*, that allows you to do about anything with poezio. Remember to use it only when you need it, and to use the functions described in this documentation only, even if much more is available. If your plugin needs to do something not available with these methods, please do a feature request instead of doing some dirty hacks with some other methods.
+
+Core methods
+^^^^^^^^^^^^
+CAUTION: TODO
+
+
+[[events-list]]
+Events list
+-----------
+CAUTION: TODO
+
+Examples
+--------
+
+[[example-1]]
+.Add a simple command that sends "Hello World!" into the conversation
+=====================================================================
+[source,python]
+---------------
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('hello', self.command_hello, "Usage: /hello\nHello: Send 'Hello World!'", None)
+
+ def command_hello(self, args):
+ self.core.send_message('Hello World!')
+---------------
+=====================================================================
+
+[[example-2]]
+
+.Adds an event handler that sends ``tg'' to a groupchat when a message is received from someone named ``Partauch''
+=====================================================================
+[source,python]
+---------------
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_event_handler('groupchat_message', self.on_groupchat_message)
+
+ def on_groupchat_message(self, message):
+ if message['mucnick'] == "Partauche":
+ self.core.send_message('tg', to=message.getMucroom())
+---------------
+=====================================================================
+
diff --git a/doc/en/usage.txt b/doc/en/usage.txt
index be6809ba..f11ab5ed 100644
--- a/doc/en/usage.txt
+++ b/doc/en/usage.txt
@@ -1,22 +1,173 @@
Usage
=====
+This page is the main page of the documentation for poezio, explaining how to
+ use it and describing its interfaces.
+
+Poezio is composed of tabs which can be of various types. Each tab type has
+a distinct interface, list of commands and list of key shortcuts, in addition
+to the global commands and key shortcuts.
+
+Tabs
+----
+On all tabs, you get a line showing the the list of all opened tabs. Each tab
+has a number, each time you open a new tab, it gets the next available number.
+
+
+image::../images/tab_bar.png[title="Example of 5 opened tabs"]
+
+The tab numbered _0_ is always the _roster_ tab, the other tabs can be of any
+type.
+
+
+The status of a tab is represented by its color:
+
+* *[navy]#blue#* (tab _0_): an inactive tab of any type, nothing new to see
+ there.
+* *[purple]#purple#* (tab _1_): a MultiUserChat tab with at least one new
+ unread message.
+* *[green]#green#* (tab _2_): a tab of a private conversation with a new
+ message to read.
+* *[teal]#cyan#* (tab _3_): the current tab.
+* *[red]#red#* (tab _4_): a MultiUserChat tab with at least one new hightlight
+ message.
+
+You can go from one tab to another in many ways:
+
+* Ctrl+n and Ctrl+p
+* <<command-win, win>> command
+* <<command-next, next>> and <<command-prev, prev>> commands
+* Alt+ a number
+* Alt+j followed by a two-digits number
+* Alt+e, this will jump to the next tab with the highest priority. Priority
+ applies in this order: private message > highlight message > normal message.
+
+Roster tab
+~~~~~~~~~~
+This is a unique tab, always numbered _0_. It contains the list of your
+contacts. You can add/remove/edit/search contacts from there, and you can open
+a conversation with one of them.
+
+Use the _arrows_ to browse the list, the _space_ key to fold or unfold a group
+or a contact.
+
+image::../images/roster.png["The roster tab", title="The roster tab"]
+
+* _1_: The area where information messages are displayed.
+* _2_: The actual list of contacts. The first level is group, the second is the
+* contacts and the third is the resources of you online contacts.
+* _3_: More informations about the selected contact.
+
+MultiUserChat tab
+~~~~~~~~~~~~~~~~~
+
+This tab contains a multi-users conversation.
+
+image::../images/muc.png["The MUC tab", title="The MUC tab"]
+
+* _1_: The conversation window, this is where all the messages and events
+ related to the muc will be displayed. It can be scrolled up and down with
+ PageUp and PageDown.
+* _2_: The participant list. Participants are listed by their role first, and
+ then alphabetically.
+ In the default theme, The nick colors mean:
+ - Red: moderator
+ - Blue: participant
+ - gray: visitor +
+ +
+ The status of each participant is symbolized using the _color_ of the
+ character on the left of its nick.
+ That character also shows the chatstate of each participant:
+ - _|_: inactive
+ - _X_: composing
+ - _A_: active
+ - _p_: paused
+* _3_: Your information in that MUC (the name of the room, your nick, your role
+ and affiliation).
+* _4_: The topic of the room.
+
+You can configure the room (if you have the rights to do it) using the
+_/configure_ command, open a private conversation with someone using the
+_/query_ command, change or view the topic using the _/topic_ command…
+
+Private tab
+~~~~~~~~~~~
+This is the tab opened with the _/query_ command, letting you talk in private
+with a participant of a multi-users chat.
+
+image::../images/private.png["The private tab", title="The private tab"]
+
+This is just a simple one to one conversation, with a line showing the status,
+name and chatstate of the participant.
+
+Conversation tab
+~~~~~~~~~~~~~~~~
+A tab opened from the roster, to talk in private with one of your contacts.
+
+image::../images/conversation.png["The conversation tab", title="The conversation tab"]
+
+This is also just a simple one to one conversation, with a line showing the status,
+name and chatstate of the participant, as well as a line at the top showing the
+status message of the contact.
+
+Dataforms tab
+~~~~~~~~~~~~~
+
+This tab lets you view a form receive from a remote entity, edit the values and
+send everything back. It is mostly used to configure MUCs with the _/configure_
+command but can actually be used for almost anything.
+
+image::../images/data_forms.png["The dataform tab", title="The dataform tab"]
+
+Use the _up_ and _down_ keys to go from one field to the other, and edit the
+value using the _space_, _left_ or _right_ keys, or by entering text.
+
+You can then send the completed form using _Ctrl+y_ or cancel using _Ctrl+g_.
+
+List tab
+~~~~~~~~
+
+This tab lists all public rooms on a MUC service. It is currently very limited
+but will be improved in the future. There currently is no way to search a room
+or even to sort them.
+
+image::../images/list.png["The list tab", title="The list tab"]
+
+Use the _up_ and _down_ or _PageUp_ and _PageDown_ keys to browse the list, and
+use _Enter_ or _j_ to join the selected room.
+
Commands
--------
-Command listing
-~~~~~~~~~~~~~~~
+Commands start with the _/_ character and can take a list of any number
+of arguments, separated by spaces. If an argument should contain a space,
+you can use the _"_ character to surround this argument.
-The commands are shown like this:
+The commands described in this page are shown like this:
+===========================================================
/command <mandatory argument> [optional argument]
+===========================================================
+
+You can get the same help as below with the _/help_ command.
+
Global commands
-^^^^^^^^^^^^^^^
+~~~~~~~~~~~~~~~
These commands work in *any* tab.
-*/join [room_name][@server][/nick] [password]*:: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you're looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.
+*/help [command]*:: If called without an argument, this command will list the
+ available commands. If it has a valid command as an argument, this command
+ will show the usage and the help for the given command.
+
+*/join [room_name][@server][/nick] [password]*:: Join the specified room. You
+ can specify a nickname after a slash (/). If no nickname is specified, you
+ will use the default_nick in the configuration file. You can omit the room
+ name: you will then join the room you're looking at (useful if you were
+ kicked). You can also provide a room_name without specifying a server, the
+ server of the room you're currently in will be used. You can also provide a
+ password to join the room.
- Examples:
* /join room@server.tld
@@ -31,29 +182,162 @@ These commands work in *any* tab.
*/quit*:: Like /exit.
+[[command-next]]
*/next*:: Go to the next room.
+[[command-prev]]
*/prev*:: Go to the previous room.
+[[command-win]]
*/win <number>*:: Go to the specified room.
*/w <number>*:: Like /win.
-*/status <availability> [status message]*:: Set your availability and (optionaly) your status message. The <availability> argument is one of "available, chat, away, afk, dnd, busy, xa" and the optional [status] argument will be your status message.'
+*/status <availability> [status message]*:: Set your availability and
+ (optionaly) your status message. The <availability> argument is one of
+ "available, chat, away, afk, dnd, busy, xa" and the optional [status] argument
+ will be your status message.'
-*/bookmark [roomname][/nick]*:: Bookmark the specified room (you will then auto-join it on each poezio start). This commands uses the same syntax as /join. Type /help join for syntax examples. Note that when typing /bookmark on its own, the room will be bookmarked with the nickname you're currently using in this room (instead of default_nick).
+*/bookmark [roomname][/nick]*:: Bookmark the specified room (you will then
+ auto-join it on each poezio start). This commands uses almost the same syntax
+ as /join. Type /help join for syntax examples. Note that when typing /bookmark
+ on its own, the room will be bookmarked with the nickname you're currently
+ using in this room (instead of default_nick).
-*/set <option> [value]*:: Sets the value to the option in your configuration file. You can, for example, change your default nickname by doing "/set default_nick toto" or your resource with "/set resource blabla". You can also set an empty value (nothing) by providing no [value] after <option>.
+*/set <option> [value]*:: Sets the value to the option in your configuration
+ file. You can, for example, change your default nickname by doing "/set
+ default_nick toto" or your resource with "/set resource blabla". You can also
+ set an empty value (nothing) by providing no [value] after <option>.
*/theme*:: Reload the theme defined in the config file.
-*/list [server.tld]*:: Get the list of public chatrooms in the specified server.
+*/list [server.tld]*:: Get the list of public chatrooms in the specified server
+.
+
+*/message <jid> [optional message]*:: Open a conversation with the specified
+ JID (event if it is not in our roster), and send a message to him, if
+ specified.
+
+*/version <jid>*:: Get the software version of the given JID (usually its XMPP
+ client and Operating System).
+
+*/server_cycle [server.tld] [message]*:: Disconnect and reconnect in all the
+ rooms of server.tld.
+
+*/bind <key> <eq>*:: Bind a key to another key or to a "command". For example,
+ "/bind ^H KEY_UP" makes Control + h behave the same way as the Up key. See the
+ link:keys.html[key bindings documentation page] for more details.
+
+NOTE: The following command will work everywhere, except in the Roster tab.
+
+*/close*:: Close the tab.
+
+Chat tab commands
+~~~~~~~~~~~~~~~~~
+
+These commands will work in any conversation tab (MultiUserChat, Private, or
+ Conversation tabs).
+
+*/say <message>*:: Just send the message (only useful it you want your message
+ to begin with a _/_). Note that you can also send message starting with a _/_
+ by starting it with _//_.
+
+MultiUserChat tab commands
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+*/ignore <nickname>*:: Ignore a specified nickname.
+
+*/unignore <nickname>*:: Remove the specified nickname from the ignore list.
+
+*/kick <nick> [reason]*:: Kick the user with the specified nickname. You can
+ also give an optional reason.
+
+*/topic <subject>*:: Change the subject of the room. You might want to knwow
+ that entering "/topic [tab]" will autocomplete the topic.
+
+*/query <nick> [message]*:: 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 be sent to this user.
+
+*/part [message]*:: Disconnect you from a room. You can specify an optional
+ message.
+
+*/close [message]*:: Disconnect you from a room (if you are connected) and
+ close the tab. You can specify an optional message if you are still connected.
+
+*/nick <nickname>*:: Change your nickname in the current room.
+
+*/recolor*:: Re-assign a color to all the participants in the current room,
+ based on the last time they talked. Use this if the participants currently
+ talking have too many identical colors.
+
+*/cycle [message]*:: Leave the current room an rejoint it immediatly. You can
+ specify an optional quit message.
+
+*/info <nickname>*:: Display some information about the user in the room:
+ his/her role, affiliation, status, and status message.
+
+*/version <nickname or jid>*:: Get the software version of the given nick in
+ room or the given jid (usually its XMPP client and Operating System).
+
+*/configure*:: Configure the current room through a form.
+
+*/names*:: Get the list of the users in the room, their number, and the list
+ of the people assuming different roles.
+
+*/clear*:: Clear the current buffer.
+
+Private tab commands
+~~~~~~~~~~~~~~~~~~~~
+
+*/info*:: Display some info about this user in the MultiUserChat.
+
+*/unquery*:: Close the tab.
+
+*/version*:: Get the software version of the current interlocutor (usually its
+ XMPP client and Operating System).
+
+Normal Conversation tab commands
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+*/info*:: Display the status of this contact.
+
+*/unquery*:: Close the tab.
+
+*/version*:: Get the software version of the current interlocutor (usually its
+ XMPP client and Operating System).
+
+Roster tab commands
+~~~~~~~~~~~~~~~~~~~
+
+*/accept [jid]*:: Authorize the provided JID (or the selected contact in the
+ roster) to see your presence.
+
+*/deny [jid]*:: Prevent the provided JID (or the selected contact in the
+ roster) from seeing your presence.
+
+*/add <jid>*:: Add the specified JID to your roster and authorize him to see
+ your presence. If he accepts you, the subscription will be mutual (and if he
+ doesn’t, you can still /deny him).
+
+*/name <jid> <name>*:: Set the given JID’s name.
+
+*/groupadd <jid> <group>*:: Add the given JID to the given group (if the group
+ does not exist, it will be created).
+
+*/groupremove <jid> <group>*:: Remove the given JID from the given group (if
+ the group is empty after that, it will get deleted).
-*/message <jid> [optional message]*:: Open a conversation with the specified JID (event if it is not in our roster), and send a message to him, if specified.
+*/remove [jid]*:: Remove the specified JID from your roster. This will
+ unsubscribe you from its presence, cancel its subscription to yours, and
+ remove the item from your roster.
-*/version <jid>*:: Get the software version of the given JID (usually its XMPP client and Operating System).
+NOTE: The following commands do not comply with any XEP or whatever, but they
+ can still prove useful when you are migrating to an other JID.
-*/server_cycle [server.tld] [message]*:: Disconnect and reconnect in all the rooms of server.tld.
+*/export [/path/to/file]*:: Export your contacts into /path/to/file if
+ specified, or $HOME/poezio_contacts if not.
-*/bind <key> <eq>*:: Bind a key to another key or to a "command". For example, "/bind ^H KEY_UP" makes Control + h behave the same way than the Up key.
+*/import [/path/to/file]*:: Import your contacts from /path/to/file if
+ specified, or $HOME/poezio_contacts if not.
diff --git a/doc/images/conversation.png b/doc/images/conversation.png
new file mode 100644
index 00000000..f5347178
--- /dev/null
+++ b/doc/images/conversation.png
Binary files differ
diff --git a/doc/images/data_forms.png b/doc/images/data_forms.png
new file mode 100644
index 00000000..d6e53cd9
--- /dev/null
+++ b/doc/images/data_forms.png
Binary files differ
diff --git a/doc/images/list.png b/doc/images/list.png
new file mode 100644
index 00000000..768ca41f
--- /dev/null
+++ b/doc/images/list.png
Binary files differ
diff --git a/doc/images/muc.png b/doc/images/muc.png
new file mode 100644
index 00000000..1e431e4a
--- /dev/null
+++ b/doc/images/muc.png
Binary files differ
diff --git a/doc/images/private.png b/doc/images/private.png
new file mode 100644
index 00000000..7d604dc2
--- /dev/null
+++ b/doc/images/private.png
Binary files differ
diff --git a/doc/images/roster.png b/doc/images/roster.png
new file mode 100644
index 00000000..d853c1cb
--- /dev/null
+++ b/doc/images/roster.png
Binary files differ
diff --git a/doc/images/tab_bar.png b/doc/images/tab_bar.png
new file mode 100644
index 00000000..fc482ffd
--- /dev/null
+++ b/doc/images/tab_bar.png
Binary files differ
diff --git a/plugins/alias.py b/plugins/alias.py
new file mode 100644
index 00000000..eb596332
--- /dev/null
+++ b/plugins/alias.py
@@ -0,0 +1,40 @@
+from plugin import BasePlugin
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('alias', self.command_alias, '/alias <alias> <command> <args>\nAlias: create an alias command')
+ self.add_command('unalias', self.command_unalias, '/unalias <alias>\nUnalias: remove a previously created alias')
+ self.commands = {}
+
+ def command_alias(self, line):
+ arg = line.split()
+ if len(arg) < 2:
+ self.core.information('Alias: Not enough parameters', 'Error')
+ return
+ alias = arg[0]
+ command = arg[1]
+ post_args = ' '.join(arg[2:])
+
+ if alias in self.core.commands or alias in self.commands:
+ self.core.information('Alias: command already exists', 'Error')
+ return
+ self.commands[alias] = lambda args: self.get_command(command)(post_args+args)
+ self.add_command(alias, self.commands[alias], 'This command is an alias for /%s %s' % (command, post_args))
+ self.core.information('Alias /%s successfuly created' % alias, 'Info')
+
+ def command_unalias(self, alias):
+ if alias in self.commands:
+ del self.commands[alias]
+ self.del_command(alias)
+ self.core.information('Alias /%s successfuly deleted' % alias, 'Info')
+
+ def get_command(self, name):
+ """Returns the function associated with a command"""
+ def dummy(args):
+ """Dummy function called if the command doesn’t exist"""
+ pass
+ if name in self.core.commands:
+ return self.core.commands[name][0]
+ elif name in self.core.current_tab().commands:
+ return self.core.current_tab().commands[name][0]
+ return dummy
diff --git a/plugins/day_change.py b/plugins/day_change.py
new file mode 100644
index 00000000..14924684
--- /dev/null
+++ b/plugins/day_change.py
@@ -0,0 +1,30 @@
+from gettext import gettext as _
+from plugin import BasePlugin
+import datetime
+import tabs
+import timed_events
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.schedule_event()
+
+ def cleanup(self):
+ self.core.remove_timed_event(self.next_event)
+
+ def schedule_event(self):
+ day_change = datetime.datetime.combine(datetime.date.today(), datetime.time())
+ day_change += datetime.timedelta(1)
+ self.next_event = timed_events.TimedEvent(day_change, self.day_change)
+ self.core.add_timed_event(self.next_event)
+
+ def day_change(self):
+ msg = datetime.date.today().strftime(_("Day changed to %x"))
+
+ for tab in self.core.tabs:
+ if (isinstance(tab, tabs.MucTab) or
+ isinstance(tab, tabs.PrivateTab) or
+ isinstance(tab, tabs.ConversationTab)):
+ tab.add_message(msg)
+
+ self.core.refresh_window()
+ self.schedule_event()
diff --git a/plugins/exec.py b/plugins/exec.py
new file mode 100644
index 00000000..f7f451df
--- /dev/null
+++ b/plugins/exec.py
@@ -0,0 +1,43 @@
+# A plugin that can execute a command and send the result in the conversation
+
+from plugin import BasePlugin
+import os
+import common
+import shlex
+import subprocess
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('exec', self.command_exec, "Usage: /exec [-o|-O] <command>\nExec: Execute a shell command and prints the result in the information buffer. The command should be ONE argument, that means it should be between \"\". The first argument (before the command) can be -o or -O. If -o is specified, it sends the result in the current conversation. If -O is specified, it sends the command and its result in the current conversation.\nExample: /exec -O \"uptime\" will send “uptime\n20:36:19 up 3:47, 4 users, load average: 0.09, 0.13, 0.09” in the current conversation.")
+
+ def command_exec(self, args):
+ args = common.shell_split(args)
+ if len(args) == 1:
+ command = args[0]
+ arg = None
+ elif len(args) == 2:
+ command = args[1]
+ arg = args[0]
+ else:
+ self.core.command_help('exec')
+ return
+ try:
+ cut_command = shlex.split(command)
+ except Exception as e:
+ self.core.information('Failed to parse command: %s' % (e,), 'Error')
+ return
+ try:
+ process = subprocess.Popen(cut_command, stdout=subprocess.PIPE)
+ except OSError as e:
+ self.core.information('Failed to execute command: %s' % (e,), 'Error')
+ return
+ result = process.communicate()[0].decode('utf-8')
+ if arg and arg == '-o':
+ if not self.core.send_message('%s' % (result,)):
+ self.core.information('Cannot send result (%s), this is not a conversation tab' % result)
+ elif arg and arg == '-O':
+ if not self.core.send_message('%s:\n%s' % (command, result)):
+ self.core.information('Cannot send result (%s), this is not a conversation tab' % result)
+ else:
+ self.core.information('%s:\n%s' % (command, result), 'Info')
+ return
diff --git a/plugins/figlet.py b/plugins/figlet.py
new file mode 100644
index 00000000..cf885352
--- /dev/null
+++ b/plugins/figlet.py
@@ -0,0 +1,13 @@
+from plugin import BasePlugin
+import subprocess
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_poezio_event_handler('muc_say', self.figletize)
+ self.add_poezio_event_handler('conversation_say', self.figletize)
+ self.add_poezio_event_handler('private_say', self.figletize)
+
+ def figletize(self, msg):
+ process = subprocess.Popen(['figlet', msg['body']], stdout=subprocess.PIPE)
+ result = process.communicate()[0].decode('utf-8')
+ msg['body'] = result
diff --git a/plugins/link.py b/plugins/link.py
new file mode 100644
index 00000000..8ef52982
--- /dev/null
+++ b/plugins/link.py
@@ -0,0 +1,47 @@
+# A plugin that adds the /link command, letting you open links that are pasted
+# in the conversation, without having to click them.
+
+import os
+import re
+
+from plugin import BasePlugin, PluginConfig
+from xhtml import clean_text
+import common
+
+url_pattern = re.compile(r'\b(http[s]?://(?:\S+))\b', re.I|re.U)
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('link', self.command_link, "Usage: /link\nLink: opens the last link from the conversation into a browser.")
+
+ def find_link(self, nb):
+ messages = self.core.get_conversation_messages()
+ if not messages:
+ return None
+ for message in messages[::-1]:
+ matches = url_pattern.findall(clean_text(message.txt))
+ if matches:
+ for url in matches[::-1]:
+ if nb == 1:
+ return url
+ else:
+ nb -= 1
+ return None
+
+ def command_link(self, args):
+ args = common.shell_split(args)
+ if len(args) == 1:
+ try:
+ nb = int(args[0])
+ except:
+ return self.core.command_help('link')
+ else:
+ nb = 1
+ link = self.find_link(nb)
+ if link:
+ self.core.exec_command('%s %s' % (self.config.get('browser', 'firefox'), link))
+ else:
+ self.core.information('No URL found.', 'Warning')
+
+ def cleanup(self):
+ del self.config
diff --git a/plugins/mpd_client.py b/plugins/mpd_client.py
new file mode 100644
index 00000000..b56e0d7f
--- /dev/null
+++ b/plugins/mpd_client.py
@@ -0,0 +1,29 @@
+# a plugin adding a command to manipulate an MPD instance
+
+from plugin import BasePlugin
+from common import shell_split
+import mpd
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('mpd', self.command_mpd, "Usage: /mpd [full]\nMpd: sends a message showing the current song of an MPD instance. If full is provided, the message is more verbose.", self.completion_mpd)
+
+ def command_mpd(self, args):
+ args = shell_split(args)
+ c = mpd.MPDClient()
+ c.connect(host=self.config.get('host', 'localhost'), port=self.config.get('port', '6600'))
+ password = self.config.get('password', '')
+ if password:
+ c.password(password)
+ current = c.currentsong()
+ current_time = float(c.status()['elapsed'])
+
+ s = '%(artist)s - %(title)s (%(album)s)' % current
+ if 'full' in args:
+ pourcentage = int(current_time / float(current['time']) * 10)
+ s += ' \x192[\x191' + '-'*(pourcentage-1) + '\x193+' + '\x191' + '-' * (10-pourcentage-1) + '\x192]\x19o'
+ if not self.core.send_message('%s' % (s,)):
+ self.core.information('Cannot send result (%s), this is not a conversation tab' % result)
+
+ def completion_mpd(self, the_input):
+ return the_input.auto_completion(['full'])
diff --git a/plugins/rainbow.py b/plugins/rainbow.py
new file mode 100644
index 00000000..0f242027
--- /dev/null
+++ b/plugins/rainbow.py
@@ -0,0 +1,20 @@
+from plugin import BasePlugin
+import xhtml
+import random
+
+possible_colors = list(range(256))
+# remove the colors that are almost white or almost black
+for col in [16, 232, 233, 234, 235, 236, 237, 15, 231, 255, 254, 253, 252, 251]:
+ possible_colors.remove(col)
+
+def rand_color():
+ return '\x19%s}' % (random.choice(possible_colors),)
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_poezio_event_handler('muc_say', self.rainbowize)
+ self.add_poezio_event_handler('private_say', self.rainbowize)
+ self.add_poezio_event_handler('conversation_say', self.rainbowize)
+
+ def rainbowize(self, msg):
+ msg['body'] = ''.join(['%s%s' % (rand_color(),char,) for char in xhtml.clean_text(msg['body'])])
diff --git a/plugins/screen_detach.py b/plugins/screen_detach.py
new file mode 100644
index 00000000..6ee96896
--- /dev/null
+++ b/plugins/screen_detach.py
@@ -0,0 +1,46 @@
+from plugin import BasePlugin
+import os
+import stat
+import pyinotify
+
+SCREEN_DIR = '/var/run/screen/S-%s' % (os.getlogin(),)
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.timed_event = None
+ sock_path = None
+ self.thread = None
+ for f in os.listdir(SCREEN_DIR):
+ path = os.path.join(SCREEN_DIR, f)
+ if screen_attached(path):
+ sock_path = path
+ self.attached = True
+ break
+
+ # Only actually do something if we found an attached screen (assuming only one)
+ if sock_path:
+ wm = pyinotify.WatchManager()
+ wm.add_watch(sock_path, pyinotify.EventsCodes.ALL_FLAGS['IN_ATTRIB'])
+ self.thread = pyinotify.ThreadedNotifier(wm, default_proc_fun=HandleScreen(plugin=self))
+ self.thread.start()
+
+ def cleanup(self):
+ if self.thread:
+ self.thread.stop()
+
+ def update_screen_state(self, socket):
+ attached = screen_attached(socket)
+ if attached != self.attached:
+ self.attached = attached
+ status = 'available' if self.attached else 'away'
+ self.core.command_status(status)
+
+def screen_attached(socket):
+ return (os.stat(socket).st_mode & stat.S_IXUSR) != 0
+
+class HandleScreen(pyinotify.ProcessEvent):
+ def my_init(self, **kwargs):
+ self.plugin = kwargs['plugin']
+
+ def process_IN_ATTRIB(self, event):
+ self.plugin.update_screen_state(event.path)
diff --git a/plugins/status.py b/plugins/status.py
new file mode 100644
index 00000000..7eb27cb5
--- /dev/null
+++ b/plugins/status.py
@@ -0,0 +1,21 @@
+from plugin import BasePlugin
+
+class Plugin(BasePlugin):
+ """
+ Adds several convenient aliases to /status command
+ """
+ def init(self):
+ self.add_command('dnd', lambda line: self.core.command_status('dnd '+line),
+ '/dnd [status message]\nDnd: Set your status as dnd (do not disturb).')
+ self.add_command('busy', lambda line: self.core.command_status('busy '+line),
+ '/busy [status message]\nBusy: Set your status as busy.')
+ self.add_command('chat', lambda line: self.core.command_status('chat '+line),
+ '/chat [status message]\nChat: Set your status as chatty.')
+ self.add_command('xa', lambda line: self.core.command_status('xa '+line),
+ '/xa [status message]\nXa: Set your status as xa (eXtended away).')
+ self.add_command('afk', lambda line: self.core.command_status('afk '+line),
+ '/afk [status message]\nAfk: Set your status as afk (away from keyboard).')
+ self.add_command('away', lambda line: self.core.command_status('away '+line),
+ '/away [status message]\nAway: Set your status as away.')
+ self.add_command('away', lambda line: self.core.command_status('away '+line),
+ '/available [status message]\nAvailable: Set your status as available.')
diff --git a/plugins/test.py b/plugins/test.py
new file mode 100644
index 00000000..0d5cdb0a
--- /dev/null
+++ b/plugins/test.py
@@ -0,0 +1,20 @@
+from plugin import BasePlugin
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_command('plugintest', self.command_plugintest, 'Test command')
+ self.add_event_handler('message', self.on_message)
+ self.core.information("Plugin loaded")
+ self.core.connect('enter', self.on_enter)
+
+ def cleanup(self):
+ self.core.information("Plugin unloaded")
+
+ def on_enter(self, line):
+ self.core.information('Text sent: {}'.format(line))
+
+ def on_message(self, message):
+ self.core.information("Test plugin received message: {}".format(message))
+
+ def command_plugintest(self, args):
+ self.core.information("Command! With args {}".format(args))
diff --git a/plugins/translate.py b/plugins/translate.py
new file mode 100644
index 00000000..520d02b4
--- /dev/null
+++ b/plugins/translate.py
@@ -0,0 +1,34 @@
+from plugin import BasePlugin
+import urllib.request
+from urllib.parse import urlencode
+import xhtml
+import json
+
+TARGET_LANG = 'en'
+
+def translate(s, target=TARGET_LANG, source=''):
+ f = urllib.request.urlopen('http://ajax.googleapis.com/ajax/services/language/translate', urlencode({ 'v': '1.0', 'q': s, 'langpair': '%s|%s' % (source, target) }))
+ response = json.loads(str(f.read(), 'utf-8'))['responseData']
+ return (response['translatedText'], response['detectedSourceLanguage'])
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.add_event_handler('groupchat_message', self.on_groupchat_message)
+
+ def on_groupchat_message(self, message):
+ try:
+ room_from = message.getMucroom()
+ if message['type'] == 'error':
+ return
+
+ if room_from == 'poezio@muc.poezio.eu':
+ nick_from = message['mucnick']
+ body = xhtml.get_body_from_message_stanza(message)
+ room = self.core.get_room_by_name(room_from)
+ text, lang = translate(body)
+ if lang != TARGET_LANG:
+ room.add_message(text, nickname=nick_from)
+ self.core.refresh_window()
+ except Exception as e:
+ import traceback
+ self.core.information("Exception in translator! %s" % (traceback.format_exc(),))
diff --git a/src/core.py b/src/core.py
index b0b4a913..e2ba8ce1 100644
--- a/src/core.py
+++ b/src/core.py
@@ -16,6 +16,8 @@ import traceback
from datetime import datetime
+from inspect import getargspec
+
import common
import theming
import logging
@@ -30,11 +32,14 @@ import multiuserchat as muc
import tabs
import xhtml
+import events
import pubsub
import windows
import connection
import timed_events
+from plugin_manager import PluginManager
+
from data_forms import DataFormsTab
from config import config, options
from logger import logger
@@ -43,6 +48,7 @@ from contact import Contact, Resource
from text_buffer import TextBuffer
from keyboard import read_char
from theming import get_theme
+from fifo import Fifo
from windows import g_lock
# http://xmpp.org/extensions/xep-0045.html#errorstatus
@@ -72,6 +78,7 @@ class Core(object):
"""
User interface using ncurses
"""
+
def __init__(self):
# All uncaught exception are given to this callback, instead
# of being displayed on the screen and exiting the program.
@@ -79,7 +86,9 @@ class Core(object):
self.status = Status(show=None, message='')
sys.excepthook = self.on_exception
self.running = True
+ self.events = events.EventHandler()
self.xmpp = singleton.Singleton(connection.Connection)
+ self.remote_fifo = None
# a unique buffer used to store global informations
# that are displayed in almost all tabs, in an
# information window.
@@ -98,6 +107,7 @@ class Core(object):
# a completion function, taking a Input as argument. Can be None)
# The completion function should return True if a completion was
# made ; False otherwise
+ self.plugin_manager = PluginManager(self)
self.commands = {
'help': (self.command_help, '\_o< KOIN KOIN KOIN', self.completion_help),
'join': (self.command_join, _("Usage: /join [room_name][@server][/nick] [password]\nJoin: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join room2\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / password"), self.completion_join),
@@ -107,20 +117,20 @@ class Core(object):
'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room."), None),
'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room."), self.completion_win),
'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), self.completion_win),
- 'show': (self.command_status, _('Usage: /show <availability> [status message]\nShow: Sets your availability and (optionaly) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message.'), self.completion_status),
- 'status': (self.command_status, _('Usage: /status <availability> [status message]\nStatus: Sets your availability and (optionaly) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message.'), self.completion_status),
- 'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optionaly) your status message. This is equivalent to '/status away [message]'"), None),
- 'busy': (self.command_busy, _("Usage: /busy [message]\nBusy: Sets your availability to busy and (optionaly) your status message. This is equivalent to '/status busy [message]'"), None),
- 'available': (self.command_avail, _("Usage: /available [message]\nAvailable: Sets your availability to available and (optionaly) your status message. This is equivalent to '/status available [message]'"), None),
- 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark the specified room (you will then auto-join it on each poezio start). This commands uses the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), None),
- 'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Sets the value to the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>."), None),
+ 'show': (self.command_status, _('Usage: /show <availability> [status message]\nShow: Sets your availability and (optionally) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status message] argument will be your status message.'), self.completion_status),
+ 'status': (self.command_status, _('Usage: /status <availability> [status message]\nStatus: Sets your availability and (optionally) your status message. The <availability> argument is one of \"available, chat, away, afk, dnd, busy, xa\" and the optional [status message] argument will be your status message.'), self.completion_status),
+ 'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark the specified room (you will then auto-join it on each poezio start). This commands uses almost the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), None),
+ 'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Set the value of the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>."), None),
'theme': (self.command_theme, _('Usage: /theme [theme_name]\nTheme: Reload the theme defined in the config file. If theme_name is provided, set that theme before reloading it.'), None),
- 'list': (self.command_list, _('Usage: /list\nList: get the list of public chatrooms on the specified server'), self.completion_list),
- 'message': (self.command_message, _('Usage: /message <jid> [optional message]\nMessage: Open a conversation with the specified JID (even if it is not in our roster), and send a message to it, if specified'), None),
- 'version': (self.command_version, _('Usage: /version <jid>\nVersion: get the software version of the given JID (usually its XMPP client and Operating System)'), None),
- 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: disconnect from the remote server if you are currently connected and then connect to it again'), None),
- 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: disconnect and reconnects in all the rooms in domain.'), None),
- 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same than the Up key.'), None),
+ 'list': (self.command_list, _('Usage: /list\nList: Get the list of public chatrooms on the specified server.'), self.completion_list),
+ 'message': (self.command_message, _('Usage: /message <jid> [optional message]\nMessage: Open a conversation with the specified JID (even if it is not in our roster), and send a message to it, if the message is specified.'), None),
+ 'version': (self.command_version, _('Usage: /version <jid>\nVersion: Get the software version of the given JID (usually its XMPP client and Operating System).'), None),
+ 'connect': (self.command_reconnect, _('Usage: /connect\nConnect: Disconnect from the remote server if you are currently connected and then connect to it again.'), None),
+ 'server_cycle': (self.command_server_cycle, _('Usage: /server_cycle [domain] [message]\nServer Cycle: Disconnect and reconnect in all the rooms in domain.'), None),
+ 'bind': (self.command_bind, _('Usage: /bind <key> <equ>\nBind: Bind a key to an other key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same as the Up key.'), None),
+ 'load': (self.command_load, _('Usage: /load <plugin>\nLoad: Load the specified plugin'), self.plugin_manager.completion_load),
+ 'unload': (self.command_unload, _('Usage: /unload <plugin>\nUnload: Unload the specified plugin'), self.plugin_manager.completion_unload),
+ 'plugins': (self.command_plugins, _('Usage: /plugins\nPlugins: Show the plugins in use.'), None),
}
self.key_func = {
@@ -165,6 +175,15 @@ class Core(object):
self.timed_events = set()
+ self.connected_events = {}
+
+ self.autoload_plugins()
+
+ def autoload_plugins(self):
+ plugins = config.get('plugins_autoload', '')
+ for plugin in plugins.split():
+ self.plugin_manager.load(plugin)
+
def start(self):
"""
Init curses, create the first tab, etc
@@ -184,8 +203,7 @@ class Core(object):
if config.get('firstrun', ''):
self.information(_(
'It seems that it is the first time you start poezio.\n' + \
- 'The configuration help is here: http://dev.louiz.org/project/poezio/doc/HowToConfigure\n' + \
- 'And the documentation for users is here: http://dev.louiz.org/project/poezio/doc/HowToUse\n' + \
+ 'The online help is here http://poezio.eu/en/documentation.php.\n' + \
'By default, you are in poezio’s chatroom, where you can ask for help or tell us how great it is.\n' + \
'Just press Ctrl-n.' \
))
@@ -547,6 +565,7 @@ class Core(object):
tab = self.open_private_window(room_from, nick_from, False)
if not tab:
return
+ self.events.trigger('private_msg', message)
body = xhtml.get_body_from_message_stanza(message)
if not body:
return
@@ -596,6 +615,7 @@ class Core(object):
When receiving "normal" messages (from someone in our roster)
"""
jid = message['from']
+ self.events.trigger('conversation_msg', message)
body = xhtml.get_body_from_message_stanza(message)
if not body:
if message['type'] == 'error':
@@ -1036,6 +1056,7 @@ class Core(object):
if tab.get_user_by_name(nick_from) and\
tab.get_user_by_name(nick_from) in tab.ignores:
return
+ self.events.trigger('muc_msg', message)
body = xhtml.get_body_from_message_stanza(message)
if body:
date = date if delayed == True else None
@@ -1117,6 +1138,34 @@ class Core(object):
def completion_status(self, the_input):
return the_input.auto_completion([status for status in possible_show], ' ')
+ def command_load(self, arg):
+ """
+ /load <plugin>
+ """
+ args = arg.split()
+ if len(args) != 1:
+ self.command_help('load')
+ return
+ filename = args[0]
+ self.plugin_manager.load(filename)
+
+ def command_unload(self, arg):
+ """
+ /unload <plugin>
+ """
+ args = arg.split()
+ if len(args) != 1:
+ self.command_help('unload')
+ return
+ filename = args[0]
+ self.plugin_manager.unload(filename)
+
+ def command_plugins(self, arg):
+ """
+ /plugins
+ """
+ self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info')
+
def command_message(self, arg):
"""
/message <jid> [message]
@@ -1393,24 +1442,6 @@ class Core(object):
msg = "%s=%s" % (option, value)
self.information(msg, 'Info')
- def command_away(self, arg):
- """
- /away [msg]
- """
- self.command_status("away "+arg)
-
- def command_busy(self, arg):
- """
- /busy [msg]
- """
- self.command_status("busy "+arg)
-
- def command_avail(self, arg):
- """
- /avail [msg]
- """
- self.command_status("available "+arg)
-
def close_tab(self, tab=None):
"""
Close the given tab. If None, close the current one
@@ -1602,3 +1633,54 @@ class Core(object):
if not self.running or self.background is True:
return
curses.doupdate()
+
+ def send_message(self, msg):
+ """
+ Function to use in plugins to send a message in the current conversation.
+ Returns False if the current tab is not a conversation tab
+ """
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return False
+ self.current_tab().command_say(msg)
+ return True
+
+ def exec_command(self, command):
+ """
+ Execute an external command on the local or a remote
+ machine, depending on the conf. For example, to open a link in a
+ browser, do exec_command("firefox http://poezio.eu"),
+ and this will call the command on the correct computer.
+ The remote execution is done by writing the command on a fifo.
+ That fifo has to be on the machine where poezio is running, and
+ accessible (through sshfs for example) from the local machine (where
+ poezio is not running). A very simple daemon reads on that fifo,
+ and executes any command that is read in it.
+ """
+ command = '%s\n' % (command,)
+ if config.get('exec_remote', 'false') == 'true':
+ # We just write the command in the fifo
+ if not self.remote_fifo:
+ try:
+ self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w')
+ except (OSError, IOError) as e:
+ self.information('Could not open fifo file for writing: %s' % (e,), 'Error')
+ return
+ try:
+ self.remote_fifo.write(command)
+ except (IOError) as e:
+ self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error')
+ self.remote_fifo = None
+ else:
+ pass
+
+ def get_conversation_messages(self):
+ """
+ Returns a list of all the messages in the current chat.
+ If the current tab is not a ChatTab, returns None.
+
+ Messages are namedtuples of the form
+ ('txt nick_color time str_time nickname user')
+ """
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return None
+ return self.current_tab().get_conversation_messages()
diff --git a/src/daemon.py b/src/daemon.py
new file mode 100755
index 00000000..f23d6b5f
--- /dev/null
+++ b/src/daemon.py
@@ -0,0 +1,50 @@
+#/usr/bin/env python3
+# Copyright 2011 Florent Le Coz <louiz@louiz.org>
+#
+# This file is part of Poezio.
+#
+# Poezio is free software: you can redistribute it and/or modify
+# it under the terms of the zlib license. See the COPYING file.
+
+"""
+This file is a standalone program that reads commands on
+stdin and executes them (each line should be a command).
+
+Usage: cat some_fifo | ./daemon.py
+
+Poezio writes commands in the fifo, and this daemon executes them on the
+local machine.
+Note that you should not start this daemon if you do not trust the remote
+machine that is running poezio, since this could make it run any (dangerous)
+command on your local machine.
+"""
+
+import sys
+import threading
+import subprocess
+
+class Executor(threading.Thread):
+ """
+ Just a class to execute commands in a thread.
+ This way, the execution can totally fail, we don’t care,
+ and we can start commands without having to wait for them
+ to return
+ """
+ def __init__(self, command):
+ threading.Thread.__init__(self)
+ self.command = command
+
+ def run(self):
+ print('executing %s' % (self.command.strip(),))
+ subprocess.call(self.command.split())
+
+def main():
+ while True:
+ line = sys.stdin.readline()
+ if line == '':
+ break
+ e = Executor(line)
+ e.start()
+
+if __name__ == '__main__':
+ main()
diff --git a/src/data_forms.py b/src/data_forms.py
index a7bbe97f..8445d3d2 100644
--- a/src/data_forms.py
+++ b/src/data_forms.py
@@ -152,6 +152,26 @@ class DummyInput(FieldInput, windows.Win):
def is_dummy(self):
return True
+class ColoredLabel(windows.Win):
+ def __init__(self, text):
+ self.text = text
+ self.color = 14
+ windows.Win.__init__(self)
+
+ def resize(self, height, width, y, x):
+ self._resize(height, width, y, x)
+
+ def set_color(self, color):
+ self.color = color
+ self.refresh()
+
+ def refresh(self):
+ with g_lock:
+ self._win.attron(curses.color_pair(self.color))
+ self.addstr(0, 0, self.text)
+ self._win.attroff(curses.color_pair(self.color))
+ self._refresh()
+
class BooleanWin(FieldInput, windows.Win):
def __init__(self, field):
FieldInput.__init__(self, field)
@@ -502,6 +522,7 @@ class FormWin(object):
for i, inp in enumerate(self.inputs):
if i >= self.height:
break
+ inp['label'].refresh()
inp['input'].refresh()
inp['label'].refresh()
if self.current_input < self.height-1:
diff --git a/src/events.py b/src/events.py
new file mode 100644
index 00000000..22d60ddf
--- /dev/null
+++ b/src/events.py
@@ -0,0 +1,70 @@
+#
+# This file is part of Poezio.
+#
+# Poezio is free software: you can redistribute it and/or modify
+# it under the terms of the zlib license. See the COPYING file.
+
+"""
+Defines the EventHandler class
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+class EventHandler(object):
+ """
+ A class keeping a list of possible events that are triggered
+ by poezio. You (a plugin for example) can add an event handler
+ associated with an event name, and whenever that event is triggered,
+ the callback is called
+ """
+ def __init__(self):
+ self.events = {
+ # when you are highlighted in a muc tab
+ 'highlight': [],
+ 'muc_say': [],
+ 'conversation_say': [],
+ 'private_say': [],
+ 'conversation_msg': [],
+ 'private_msg': [],
+ 'muc_msg': [],
+ }
+
+ def add_event_handler(self, name, callback, position=0):
+ """
+ Add a callback to a given event.
+ Note that if that event name doesn’t exist, it just returns False.
+ If it was successfully added, it returns True
+ position: 0 means insert a the beginning, -1 means end
+ """
+ if name not in self.events:
+ return False
+
+ if position >= 0:
+ self.events[name].insert(position, callback)
+ else:
+ self.events[name].append(callback)
+
+ return True
+
+ def trigger(self, name, *args, **kwargs):
+ """
+ Call all the callbacks associated to the given event name
+ """
+ callbacks = self.events[name]
+ for callback in callbacks:
+ callback(*args, **kwargs)
+
+ def del_event_handler(self, name, callback):
+ """
+ Remove the callback from the list of callbacks of the given event
+ """
+ if not name:
+ for event in self.events:
+ while callback in self.events[event]:
+ self.events[event].remove(callback)
+ return True
+ else:
+ if callback in self.events[name]:
+ self.events[name].remove(callback)
+
diff --git a/src/fifo.py b/src/fifo.py
new file mode 100644
index 00000000..8306e24b
--- /dev/null
+++ b/src/fifo.py
@@ -0,0 +1,70 @@
+# Copyright 2011 Florent Le Coz <louiz@louiz.org>
+#
+# This file is part of Poezio.
+#
+# Poezio is free software: you can redistribute it and/or modify
+# it under the terms of the zlib license. See the COPYING file.
+
+"""
+Defines the Fifo class
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+import os
+import threading
+
+class OpenTrick(threading.Thread):
+ """
+ A threaded trick to make the open for writing succeed.
+ A fifo cannot be opened for writing if it has not been
+ yet opened by the other hand for reading.
+ So, we just open the fifo for reading and close it
+ immediately afterwards.
+ Once that is done, we can freely keep the fifo open for
+ writing and write things in it. The writing can fail if
+ there’s still nothing reading that fifo, but we just yell
+ an error in that case.
+ """
+ def __init__(self, path):
+ threading.Thread.__init__(self)
+ self.path = path
+
+ def run(self):
+ open(self.path, 'r').close()
+
+
+class Fifo(object):
+ """
+ Just a simple file handler, writing and reading in a fifo.
+ Mode is either 'r' or 'w', just like the mode for the open()
+ function.
+ """
+ def __init__(self, path, mode):
+ self.trick = None
+ if not os.path.exists(path):
+ os.mkfifo(path)
+ if mode == 'w':
+ self.trick = OpenTrick(path)
+ # that thread will wait until we open it for writing
+ self.trick.start()
+ self.fd = open(path, mode)
+
+ def write(self, data):
+ """
+ Try to write on the fifo. If that fails, this means
+ that nothing has that fifo opened, so the writing is useless,
+ so we just return (and display an error telling that, somewhere).
+ """
+ self.fd.write(data)
+ self.fd.flush()
+
+ def readline(self):
+ return self.fd.readline()
+
+ def __del__(self):
+ try:
+ self.fd.close()
+ except:
+ pass
diff --git a/src/plugin.py b/src/plugin.py
new file mode 100644
index 00000000..80bc4dfc
--- /dev/null
+++ b/src/plugin.py
@@ -0,0 +1,92 @@
+import os
+from configparser import ConfigParser
+import config
+import inspect
+import traceback
+
+class PluginConfig(config.Config):
+ def __init__(self, filename):
+ ConfigParser.__init__(self)
+ self.__config_file__ = filename
+ self.read()
+
+ def read(self):
+ """Read the config file"""
+ ConfigParser.read(self, self.__config_file__)
+
+ def write(self):
+ """Write the config to the disk"""
+ try:
+ fp = open(self.__config_file__, 'w')
+ ConfigParser.write(self, fp)
+ fp.close()
+ return True
+ except IOError:
+ return False
+
+
+class SafetyMetaclass(type):
+ # A hack
+ core = None
+
+ @staticmethod
+ def safe_func(f):
+ def helper(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except:
+ if inspect.stack()[1][1] == inspect.getfile(f):
+ raise
+ elif SafetyMetaclass.core:
+ SafetyMetaclass.core.information(traceback.format_exc())
+ return None
+ return helper
+
+ def __new__(meta, name, bases, class_dict):
+ for k, v in class_dict.items():
+ if inspect.isfunction(v):
+ class_dict[k] = SafetyMetaclass.safe_func(v)
+ return type.__new__(meta, name, bases, class_dict)
+
+class BasePlugin(object, metaclass=SafetyMetaclass):
+ """
+ Class that all plugins derive from. Any methods beginning with command_
+ are interpreted as a command and beginning with on_ are interpreted as
+ event handlers
+ """
+
+ def __init__(self, plugin_manager, core, plugins_conf_dir):
+ self.core = core
+ # More hack; luckily we'll never have more than one core object
+ SafetyMetaclass.core = core
+ self.plugin_manager = plugin_manager
+ conf = os.path.join(plugins_conf_dir, self.__module__+'.cfg')
+ self.config = PluginConfig(conf)
+ self.init()
+
+ def init(self):
+ pass
+
+ def cleanup(self):
+ pass
+
+ def unload(self):
+ self.cleanup()
+
+ def add_command(self, name, handler, help, completion=None):
+ return self.plugin_manager.add_command(self.__module__, name, handler, help, completion)
+
+ def del_command(self, name):
+ return self.plugin_manager.del_command(self.__module__, name)
+
+ def add_event_handler(self, event_name, handler):
+ return self.plugin_manager.add_event_handler(self.__module__, event_name, handler)
+
+ def del_event_handler(self, event_name, handler):
+ return self.plugin_manager.del_event_handler(self.__module__, event_name, handler)
+
+ def add_poezio_event_handler(self, event_name, handler, position=0):
+ return self.plugin_manager.add_poezio_event_handler(self.__module__, event_name, handler, position)
+
+ def del_poezio_event_handler(self, event_name, handler):
+ return self.plugin_manager.del_poezio_event_handler(self.__module__, event_name, handler)
diff --git a/src/plugin_manager.py b/src/plugin_manager.py
new file mode 100644
index 00000000..bdf94a5b
--- /dev/null
+++ b/src/plugin_manager.py
@@ -0,0 +1,137 @@
+import imp
+import os
+import sys
+from config import config
+from gettext import gettext as _
+
+plugins_dir = config.get('plugins_dir', '')
+plugins_dir = plugins_dir or\
+ os.path.join(os.environ.get('XDG_DATA_HOME') or\
+ os.path.join(os.environ.get('HOME'), '.local', 'share'),
+ 'poezio', 'plugins')
+
+config_home = os.environ.get("XDG_CONFIG_HOME")
+if not config_home:
+ config_home = os.path.join(os.environ.get('HOME'), '.config')
+plugins_conf_dir = os.path.join(config_home, 'poezio', 'plugins')
+
+try:
+ os.makedirs(plugins_dir)
+except OSError:
+ pass
+
+try:
+ os.makedirs(plugins_conf_dir)
+except OSError:
+ pass
+
+sys.path.append(plugins_dir)
+
+class PluginManager(object):
+ def __init__(self, core):
+ self.core = core
+ self.modules = {} # module name -> module object
+ self.plugins = {} # module name -> plugin object
+ self.commands = {} # module name -> dict of commands loaded for the module
+ self.event_handlers = {} # module name -> list of event_name/handler pairs loaded for the module
+ self.poezio_event_handlers = {}
+
+ def load(self, name):
+ if name in self.plugins:
+ self.unload(name)
+
+ try:
+ if name in self.modules:
+ imp.acquire_lock()
+ module = imp.reload(self.modules[name])
+ imp.release_lock()
+ else:
+ file, filename, info = imp.find_module(name, [plugins_dir])
+ imp.acquire_lock()
+ module = imp.load_module(name, file, filename, info)
+ imp.release_lock()
+ except Exception as e:
+ import traceback
+ self.core.information(_("Could not load plugin: ") + traceback.format_exc(), 'Error')
+ return
+ finally:
+ if imp.lock_held():
+ imp.release_lock()
+
+ self.modules[name] = module
+ self.commands[name] = {}
+ self.event_handlers[name] = []
+ self.poezio_event_handlers[name] = []
+ self.plugins[name] = module.Plugin(self, self.core, plugins_conf_dir)
+
+ def unload(self, name):
+ if name in self.plugins:
+ try:
+ for command in self.commands[name].keys():
+ del self.core.commands[command]
+ for event_name, handler in self.event_handlers[name]:
+ self.core.xmpp.del_event_handler(event_name, handler)
+ for handler in self.poezio_event_handlers[name]:
+ self.core.events.del_event_handler(None, handler)
+
+ self.plugins[name].unload()
+ del self.plugins[name]
+ del self.commands[name]
+ del self.event_handlers[name]
+ del self.poezio_event_handlers[name]
+ except Exception as e:
+ import traceback
+ self.core.information(_("Could not unload plugin (may not be safe to try again): ") + traceback.format_exc())
+
+ def del_command(self, module_name, name):
+ if name in self.commands[module_name]:
+ del self.commands[module_name][name]
+ if name in self.core.commands:
+ del self.core.commands[name]
+
+ def add_command(self, module_name, name, handler, help, completion=None):
+ if name in self.core.commands:
+ raise Exception(_("Command '%s' already exists") % (name,))
+
+ commands = self.commands[module_name]
+ commands[name] = (handler, help, completion)
+ self.core.commands[name] = (handler, help, completion)
+
+ def add_event_handler(self, module_name, event_name, handler):
+ eh = self.event_handlers[module_name]
+ eh.append((event_name, handler))
+ self.core.xmpp.add_event_handler(event_name, handler)
+
+ def del_event_handler(self, module_name, event_name, handler):
+ self.core.xmpp.del_event_handler(event_name, handler)
+ eh = self.event_handlers[module_name]
+ eh = list(filter(lambda e : e != (event_name, handler), eh))
+
+ def add_poezio_event_handler(self, module_name, event_name, handler, position):
+ eh = self.poezio_event_handlers[module_name]
+ eh.append(handler)
+ self.core.events.add_event_handler(event_name, handler, position)
+
+ def del_poezio_event_handler(self, module_name, event_name, handler):
+ self.core.events.del_event_handler(None, handler)
+ eh = self.poezio_event_handlers[module_name]
+ eh = list(filter(lambda e : e != handler, eh))
+
+ def completion_load(self, the_input):
+ """
+ completion function that completes the name of the plugins, from
+ all .py files in plugins_dir
+ """
+ try:
+ names = os.listdir(plugins_dir)
+ except OSError as e:
+ self.core.information(_('Completion failed: %s' % e), 'Error')
+ return
+ plugins_files = [name[:-3] for name in names if name.endswith('.py')]
+ return the_input.auto_completion(plugins_files, '')
+
+ 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()), '')
diff --git a/src/tabs.py b/src/tabs.py
index 5d7f2286..6f100741 100644
--- a/src/tabs.py
+++ b/src/tabs.py
@@ -293,7 +293,7 @@ class ChatTab(Tab):
self.key_func['^M'] = self.on_enter
self.commands['say'] = (self.command_say,
_("""Usage: /say <message>\nSay: Just send the message.
- Useful if you want your message to begin with a '/'"""), None)
+ Useful if you want your message to begin with a '/'."""), None)
self.chat_state = None
def last_words_completion(self):
@@ -390,6 +390,9 @@ class ChatTab(Tab):
self.text_win.refresh()
self.input.refresh()
+ def get_conversation_messages(self):
+ return self._text_buffer.messages
+
def command_say(self, line):
raise NotImplementedError
@@ -427,20 +430,20 @@ class MucTab(ChatTab):
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)
- self.commands['close'] = (self.command_close, _("Usage: /close [message]\nClose: disconnect from a room and close the tab. You can specify an optional message if you are still connected."), None)
- self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room"), None)
+ self.commands['affiliation'] = (self.command_affiliation, _("Usage: /affiliation <nick> <affiliation> [reason]\nAffiliation: Set the affiliation of an user. Affiliations can be: none, member, admin, owner. You also can give an optional reason."), None)
+ self.commands['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)
+ self.commands['close'] = (self.command_close, _("Usage: /close [message]\nClose: Disconnect from a room and close the tab. You can specify an optional message if you are still connected."), None)
+ self.commands['nick'] = (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room."), None)
self.commands['recolor'] = (self.command_recolor, _('Usage: /recolor\nRecolor: Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'), None)
- self.commands['cycle'] = (self.command_cycle, _('Usage: /cycle [message]\nCycle: Leaves the current room and rejoin it immediately'), None)
- self.commands['info'] = (self.command_info, _('Usage: /info <nickname>\nInfo: Display some information about the user in the MUC: his/here role, affiliation, status and status message.'), None)
+ self.commands['cycle'] = (self.command_cycle, _('Usage: /cycle [message]\nCycle: Leave the current room and rejoin it immediately.'), None)
+ self.commands['info'] = (self.command_info, _('Usage: /info <nickname>\nInfo: Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'), None)
self.commands['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['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)
+ _('Usage: /clear\nClear: Clear the current buffer.'), None)
self.resize()
def scroll_user_list_up(self):
@@ -658,7 +661,12 @@ class MucTab(ChatTab):
if not len(args):
self.core.command_help('kick')
else:
- self.command_role(arg+ ' none')
+ if len(args) > 1:
+ msg = ' '+args[1]
+ self.core.information("-%s-" % msg)
+ else:
+ msg = ''
+ self.command_role(args[0]+ ' none'+msg)
def command_role(self, arg):
"""
@@ -711,6 +719,10 @@ class MucTab(ChatTab):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'groupchat'
msg['body'] = line
+ # trigger the event BEFORE looking for colors.
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('muc_say', msg)
if msg['body'].find('\x19') != -1:
msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body'])
msg['body'] = xhtml.clean_text(msg['body'])
@@ -1185,10 +1197,10 @@ class PrivateTab(ChatTab):
# keys
self.key_func['^I'] = self.completion
# commands
- self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Display some information about the user in the MUC: '), None)
- self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
- self.commands['close'] = (self.command_unquery, _("Usage: /close\nClose: close the tab"), None)
- self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: get the software version of the current interlocutor (usually its XMPP client and Operating System)'), None)
+ self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'), None)
+ self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: Close the tab."), None)
+ self.commands['close'] = (self.command_unquery, _("Usage: /close\nClose: Close the tab."), None)
+ self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: Get the software version of the current interlocutor (usually its XMPP client and Operating System).'), None)
self.resize()
self.parent_muc = self.core.get_tab_by_name(JID(name).bare, MucTab)
self.on = True
@@ -1202,6 +1214,10 @@ class PrivateTab(ChatTab):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
msg['body'] = line
+ # trigger the event BEFORE looking for colors.
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('private_say', msg)
self.core.add_message_to_text_buffer(self._text_buffer, msg['body'], None, self.core.own_nick or self.own_nick)
if msg['body'].find('\x19') != -1:
msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body'])
@@ -1388,15 +1404,16 @@ class RosterInfoTab(Tab):
self.key_func["o"] = self.toggle_offline_show
self.key_func["s"] = self.start_search
self.key_func["S"] = self.start_search_slow
- 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)
+ self.commands['deny'] = (self.command_deny, _("Usage: /deny [jid]\nDeny: 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: Allow the provided JID (or the selected contact in your roster), to see your presence."), self.completion_deny)
+ self.commands['add'] = (self.command_add, _("Usage: /add <jid>\nAdd: Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence."), 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: Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster."), self.completion_remove)
+ self.commands['export'] = (self.command_export, _("Usage: /export [/path/to/file]\nExport: Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not."), None)
+ self.commands['import'] = (self.command_import, _("Usage: /import [/path/to/file]\nImport: Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not."), None)
+ self.commands['clear_infos'] = (self.command_clear_infos, _("Usage: /clear_infos\nClear Infos: Use this command to clear the info buffer."), None)
self.resize()
def resize(self):
@@ -1417,6 +1434,15 @@ class RosterInfoTab(Tab):
not self.input.help_message:
self.complete_commands(self.input)
+ def command_clear_infos(self, arg):
+ """
+ /clear_infos
+ """
+ self.core.information_buffer.messages = []
+ self.information_win.rebuild_everything(self.core.information_buffer)
+ self.core.information_win.rebuild_everything(self.core.information_buffer)
+ self.refresh()
+
def command_deny(self, args):
"""
Denies a JID from our roster
@@ -1851,10 +1877,10 @@ class ConversationTab(ChatTab):
# keys
self.key_func['^I'] = self.completion
# commands
- self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None)
- self.commands['close'] = (self.command_unquery, _("Usage: /close\Close: close the tab"), None)
- self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: get the software version of the current interlocutor (usually its XMPP client and Operating System)'), None)
- self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: get the status of the contact.'), None)
+ self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: Close the tab."), None)
+ self.commands['close'] = (self.command_unquery, _("Usage: /close\Close: Close the tab."), None)
+ self.commands['version'] = (self.command_version, _('Usage: /version\nVersion: Get the software version of the current interlocutor (usually its XMPP client and Operating System).'), None)
+ self.commands['info'] = (self.command_info, _('Usage: /info\nInfo: Get the status of the contact.'), None)
self.resize()
def completion(self):
@@ -1864,6 +1890,11 @@ class ConversationTab(ChatTab):
msg = self.core.xmpp.make_message(self.get_name())
msg['type'] = 'chat'
msg['body'] = line
+ # trigger the event BEFORE looking for colors.
+ # and before displaying the message in the window
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('conversation_say', msg)
self.core.add_message_to_text_buffer(self._text_buffer, msg['body'], None, self.core.own_nick)
if msg['body'].find('\x19') != -1:
msg['xhtml_im'] = xhtml.poezio_colors_to_html(msg['body'])
@@ -1977,6 +2008,9 @@ class ConversationTab(ChatTab):
if config.get('send_chat_states', 'true') == 'true':
self.send_chat_state('gone')
+ def add_message(self, txt, time=None, nickname=None, forced_user=None):
+ self._text_buffer.add_message(txt, time, nickname, None, None, forced_user)
+
class MucListTab(Tab):
"""
A tab listing rooms from a specific server, displaying various information,
@@ -2000,7 +2034,7 @@ class MucListTab(Tab):
self.key_func['j'] = self.join_selected
self.key_func['J'] = self.join_selected_no_focus
self.key_func['^M'] = self.join_selected
- self.commands['close'] = (self.close, _("Usage: /close\nClose: Just close this tab"), None)
+ self.commands['close'] = (self.close, _("Usage: /close\nClose: Just close this tab."), None)
self.resize()
def refresh(self):
diff --git a/src/windows.py b/src/windows.py
index c94de15e..2253b871 100644
--- a/src/windows.py
+++ b/src/windows.py
@@ -367,14 +367,14 @@ class ConversationInfoWin(InfoWin):
The line above the information window, displaying informations
about the user we are talking to
"""
- color_show = {'xa':get_theme().COLOR_STATUS_XA,
- 'none':get_theme().COLOR_STATUS_ONLINE,
- '':get_theme().COLOR_STATUS_ONLINE,
- 'available':get_theme().COLOR_STATUS_ONLINE,
- 'dnd':get_theme().COLOR_STATUS_DND,
- 'away':get_theme().COLOR_STATUS_AWAY,
- 'chat':get_theme().COLOR_STATUS_CHAT,
- 'unavailable':get_theme().COLOR_STATUS_UNAVAILABLE
+ color_show = {'xa': lambda: get_theme().COLOR_STATUS_XA,
+ 'none': lambda: get_theme().COLOR_STATUS_ONLINE,
+ '': lambda: get_theme().COLOR_STATUS_ONLINE,
+ 'available': lambda: get_theme().COLOR_STATUS_ONLINE,
+ 'dnd': lambda: get_theme().COLOR_STATUS_DND,
+ 'away': lambda: get_theme().COLOR_STATUS_AWAY,
+ 'chat': lambda: get_theme().COLOR_STATUS_CHAT,
+ 'unavailable': lambda: get_theme().COLOR_STATUS_UNAVAILABLE
}
def __init__(self):
@@ -416,7 +416,7 @@ class ConversationInfoWin(InfoWin):
presence = "unavailable"
else:
presence = resource.get_presence()
- color = RosterWin.color_show[presence]
+ color = RosterWin.color_show[presence]()
self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(" ", to_curses_attr(color))
self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
@@ -1328,14 +1328,14 @@ class VerticalSeparator(Win):
self.rewrite_line()
class RosterWin(Win):
- color_show = {'xa':get_theme().COLOR_STATUS_XA,
- 'none':get_theme().COLOR_STATUS_ONLINE,
- '':get_theme().COLOR_STATUS_ONLINE,
- 'available':get_theme().COLOR_STATUS_ONLINE,
- 'dnd':get_theme().COLOR_STATUS_DND,
- 'away':get_theme().COLOR_STATUS_AWAY,
- 'chat':get_theme().COLOR_STATUS_CHAT,
- 'unavailable':get_theme().COLOR_STATUS_UNAVAILABLE
+ color_show = {'xa': lambda: get_theme().COLOR_STATUS_XA,
+ 'none': lambda: get_theme().COLOR_STATUS_ONLINE,
+ '': lambda: get_theme().COLOR_STATUS_ONLINE,
+ 'available': lambda: get_theme().COLOR_STATUS_ONLINE,
+ 'dnd':lambda: get_theme().COLOR_STATUS_DND,
+ 'away': lambda: get_theme().COLOR_STATUS_AWAY,
+ 'chat': lambda: get_theme().COLOR_STATUS_CHAT,
+ 'unavailable': lambda: get_theme().COLOR_STATUS_UNAVAILABLE
}
def __init__(self):
@@ -1473,7 +1473,7 @@ class RosterWin(Win):
else:
presence = resource.get_presence()
nb = ' (%s)' % (contact.get_nb_resources(),)
- color = RosterWin.color_show[presence]
+ color = RosterWin.color_show[presence]()
if contact.get_name():
display_name = '%s (%s)%s' % (contact.get_name(),
contact.get_bare_jid(), nb,)
@@ -1496,7 +1496,7 @@ class RosterWin(Win):
"""
Draw a specific resource line
"""
- color = RosterWin.color_show[resource.get_presence()]
+ color = RosterWin.color_show[resource.get_presence()]()
self.addstr(y, 4, " ", to_curses_attr(color))
if colored:
self.addstr(y, 6, resource.get_jid().full, to_curses_attr(get_theme().COLOR_SELECTED_ROW))
diff --git a/src/xhtml.py b/src/xhtml.py
index 44195f90..99e0bf01 100644
--- a/src/xhtml.py
+++ b/src/xhtml.py
@@ -255,7 +255,6 @@ def xhtml_to_poezio_colors(text):
key, value = rule.split(':', 1)
key = key.strip()
value = value.strip()
- log.debug(value)
if key == 'background-color':
pass#shell += '\x191'
elif key == 'color':
@@ -278,7 +277,6 @@ def xhtml_to_poezio_colors(text):
def trim(string):
return re.sub(whitespace_re, ' ', string)
- log.debug(text)
xml = ET.fromstring(text)
message = ''
if version_info[1] == 2: