diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | README | 3 | ||||
-rw-r--r-- | data/default_config.cfg | 12 | ||||
-rw-r--r-- | doc/en/configure.txt | 265 | ||||
-rw-r--r-- | doc/en/install.txt | 94 | ||||
-rw-r--r-- | doc/en/keys.txt | 66 | ||||
-rw-r--r-- | doc/en/themes.txt | 89 | ||||
-rw-r--r-- | doc/images/theme_256_colors.png | bin | 0 -> 44763 bytes | |||
-rw-r--r-- | doc/poezio.txt | 83 | ||||
-rw-r--r-- | src/core.py | 177 | ||||
-rw-r--r-- | src/room.py | 139 | ||||
-rw-r--r-- | src/tabs.py | 605 | ||||
-rw-r--r-- | src/text_buffer.py | 7 | ||||
-rw-r--r-- | src/windows.py | 47 |
14 files changed, 1087 insertions, 504 deletions
@@ -34,5 +34,9 @@ uninstall: rm -rf $(DESTDIR)$(DATADIR)/poezio rm -rf $(DESTDIR)$(MANDIR)/man1/poezio.1 +doc: + find doc -name \*.txt -exec asciidoc {} \; pot: xgettext src/*.py --from-code=utf-8 --keyword=_ -o locale/poezio.pot + +.PHONY : doc
\ No newline at end of file @@ -28,6 +28,9 @@ You need python 3.0 (and the associated devel package, to build C modules) or higher, and the SleekXMPP python library. In the developpement version, you’ll need this fork of SleekXMPP http://github.com/louiz/SleekXMPP. +Additionally, you’ll need asciidoc to build the html documentation pages. +You can read the documentation using the .txt files, as well, if you don’t +have asciidoc, or read it on the web. The simplest way to have up-to-date dependencies and to be able to test this developpement version is to use the update.sh script that downloads diff --git a/data/default_config.cfg b/data/default_config.cfg index 8ff4805d..e803a7d1 100644 --- a/data/default_config.cfg +++ b/data/default_config.cfg @@ -112,9 +112,9 @@ alternative_nickname = # Limit the number of messages you want to receive when the # multiuserchat rooms send you recent history # 0: You won't receive any -# -1: You will receive the maximum (default) +# -1: You will receive the maximum # n: You will receive at most n messages -muc_history_length = -1 +muc_history_length = 50 # set to 'true' if you want to save logs of all the messages # in files. @@ -156,7 +156,7 @@ beep_on = highlight private # Theme # If themes_dir is not set, logs will searched for in $XDG_DATA_HOME/poezio/themes, -# i.e. in ~/.local/share/poezio/themes/. Si you should specify the directory you +# i.e. in ~/.local/share/poezio/themes/. So you should specify the directory you # want to use instead. This directory will be created at startup if it doesn't # exist themes_dir = @@ -165,7 +165,11 @@ themes_dir = # in the theme_dir directory. # If the file is not found (or no filename is specified) the default # theme will be used instead -theme = poezio +theme = + +# The nick of people who join, part, change their status, etc. in a MUC will +# be displayed using their nick color if true. +display_user_color_in_join_part = false # if true, chat states will be sent to the people you are talking to. # Chat states are, for example, messages informing that you are composing diff --git a/doc/en/configure.txt b/doc/en/configure.txt new file mode 100644 index 00000000..94f8e121 --- /dev/null +++ b/doc/en/configure.txt @@ -0,0 +1,265 @@ +Configure +========= + +The configuration is located in the file *~/.config/poezio/poezio.cfg* +On its first startup, poezio will create that file (and its containing +directories) with the default configuration. You can edit that file manually +or use the */set* command to edit some of its values directly from poezio. +This file is also used to configure key bindings, but this is explained +in the _keys_ documentation file. + +That file is read at each startup and the configuration is saved when poezio +is closed. + +This configuration file *requires* all the options to be in a section +named [Poezio]. + +An option is formatted with the form +====================== +option = value +====================== + +An empty value *doesn’t* mean that the default value will be used. That’s +just an empty value. To use the default value, just comment or remove the +option entirely. + +Here is a list of all the avalaible configuration options, their meaning +and their default value. + +Configuration options +--------------------- + +[horizontal] +*server*:: anon.louiz.org + + The server to use for *anonymous* authentication. + Make sure it accepts anonymous authentification + Note that this option doesn’t do anything at all if you’re using your own JID. + +*port*:: 5222 + + The port you’ll use to connect. + +*resource*:: [empty] + + the resource you will use + If it's empty, your resource will be chosen (most likely randomly) by the server + It is not recommended to use a resource that is easy to guess, because it can lead + to presence leak. + + +*default_nick*:: [empty] + + the nick you will use when joining a room with no associated nick + If this is empty, the $USER environnement variable will be used + + +*jid*:: [empty] + + Jabber identifiant. Specify it only if you want to connect using an existing + account on a server. This is optional and useful only for some features, + like room administration, nickname registration. + The 'server' option will be ignored if you specify a JID (Jabber identifiant) + It should be in the form nickname@server.tld + +*password*:: [empty] + + A password is needed only if you specified a jid. It will be ignored otherwise + If you leave this empty, the password will be asked at each startup + + + +*rooms*:: poezio@muc.poezio.eu + + 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 + +*completion*:: normal + + the completion type you will use to complete nicknames + if "normal", complete the entire name to the first available completion + and then cycle through the possible completion with the next TABs + if "shell", if there's more than one nick for this completion, complete + only the part that all then nicks have in common (like in a shell) + + +*after_completion*:: , + + what will be put after the name, when using autocompletion + a SPACE will always be added after that + +*highlight_on*:: [empty] + + a list of words (separated by a colon (:)) that will be + highlighted if said by someone on a room + +*enable_xhtml_im*:: true + + XHTML-IM is an XMPP extension letting users send messages + containing XHTML and CSS formating. We can use this to make + colored text for example. + It is disabled by default because this is only in an experimental + state: you could miss some part of a message (mainly the URL) + but you can still send colored messages. You just won’t be able te see + the colors, though + Set to true if you want to see colored messages + +*hide_status_change*:: 120 + + Set a number for this setting. + The join OR status-change notices will be + displayed according to this number. + -1: the notices will ALWAYS be displayed + 0: the notices will NEVER be displayed + n: On any other number, the notices will only be displayed + if the user involved has talked since the last n seconds + if the value is incorrect, -1 is assumed + Default setting means : + - status changes won't be displayed unless + the user talked in the last 2 minutes + +*hide_exit_join*:: -1 + + Exact same thing than hide_status_change, except that it concerns + the quit message, and that it will be hidden only if the value is 0. + Default setting means: + - all quit and join notices will be displayed + + +*information_buffer_popup_on*:: error roster warning help info + + Some informational messages (error, a contact getting connected, etc) + are sometimes added to the information buffer. These settings can make + that buffer grow temporarly so you can read these information when they + appear. + + A list of message types that should make the information buffer grow + Possible values; error, roster, warning, info, help + +*popup_time*:: 4 + + The time the message will be visible in the information buffer when it + pops up. + If the message takes more than one line, the popup will stay visible + two more second per additional lines + +*autorejoin*:: false + + set to 'true' if you want to automatically rejoin the + room when you're kicked + +*alternative_nickname*:: [empty] + + If you want poezio to join + the room with an alternative nickname when + your nickname is already in use in the room you + wanted to join, put a non-empty value. + Else, poezio won't join the room + This value will be added to your nickname to + create the alternative nickname. + For example, if you set "\_", and wanted to use + the nickname "john", your alternative nickname + will be "john_" + +*muc_history_length*:: 50 + + Limit the number of messages you want to receive when the + multiuserchat rooms send you recent history + 0: You won't receive any + -1: You will receive the maximum + n: You will receive at most n messages + Note that if you set a huge number (like the default value), you + may not receive that much messages. The server has its own + maximum too + +*use_log*:: true + + set to 'false' if you don’t want to save logs of all the messages + in files. + + +*log_dir*:: [empty] + + If log_dir is not set, logs will be saved in $XDG_DATA_HOME/poezio/logs, + i.e. in ~/.local/share/poezio/logs/. So, you should specify the directory + you want to use instead. This directory will be created if it doesn't exist + +*show_inactive_tabs*:: true + + If you want to show all the tabs in the Tab bar, even those + with no activity, set to true. Else, set to false + + +*beep_on*:: highlight private + + The terminal can beep on various event. Put the event you want in a list + (separated by spaces). + The events can be + - highlight (when you are highlighted in a MUC) + - private (when a new private message is received, from your contacts or + someone from a MUC) + - message (any message from a MUC) + +*themes_dir*:: [empty] + + If themes_dir is not set, themes will searched for in $XDG_DATA_HOME/poezio/themes, + i.e. in ~/.local/share/poezio/themes/. So you should specify the directory you + want to use instead. This directory will be created at startup if it doesn't + exist + + +*theme*:: [empty] + + The name of the theme file (without the .py extension) that will be used. + The file should be located in the theme_dir directory. + If the file is not found (or no filename is specified) the default + theme will be used instead + +*send_chat_states*:: true + + if true, chat states will be sent to the people you are talking to. + Chat states are, for example, messages informing that you are composing + a message or that you closed the tab, etc + Set to false if you don't want people to know these information + Note that you won’t receive the chat states of your contacts + if you don't send yours. + + +*send_poezio_info*:: true + + if true, information about the software (name and version) + will be sent if requested by anyone + Set to false if you don't want people to know these information + + +*send_os_info*:: true + + if true, information about the Operation System you're using + will be sent when requested by anyone + Set to false if you don't want people to know these information + Note that this information will not be sent if send_poezio_info is False + +*send_time*:: true + + if true, your current time will be sent if asked + Set to false if you don't want people to know that information + + +*max_messages_in_memory*:: 2048 + + Configure the number of maximum messages (for each tab) that + can be kept in memory. If poezio consumes too much memory, lower these + values + +*max_lines_in_memory*:: 2048 + + Configure the number of maximum lines (for each tab) that + can be kept in memory. If poezio consumes too much memory, lower these + values + +*lazy_resize*:: true + + Defines if all tabs are resized at the same time (if set to false) + or if they are really resized only when needed (if set to true). + “true” should be the most comfortable value diff --git a/doc/en/install.txt b/doc/en/install.txt new file mode 100644 index 00000000..75cc0ea8 --- /dev/null +++ b/doc/en/install.txt @@ -0,0 +1,94 @@ +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) +* *Debian*: Use an other distro. + +(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: +============================ +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. + +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. + + +Download SleekXMPP +============================ +git clone git://github.com/louiz/SleekXMPP.git +============================ + +Make sure you're using the develop branch by typing +============================ +cd SleekXMPP + +git checkout develop +============================ + +Install SleekXMPP with +============================ +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). +============================ +hg clone http://hg.louiz.org/dnspython + +cd dnspython +============================ + +And do the same again: +============================ +python3 setup.py build + +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: +============================ +sh update.sh +============================ + +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: +============================ + ./launch.sh +============================ + + +To install poezio, do, as root (or sudo with ubuntu or whatever): +============================ +make install +============================ + +And then start it with: +============================ +poezio +============================ + diff --git a/doc/en/keys.txt b/doc/en/keys.txt new file mode 100644 index 00000000..eaae961d --- /dev/null +++ b/doc/en/keys.txt @@ -0,0 +1,66 @@ +Keys +==== + +This file describes the default keys of poezio and explains how to +configure them. + +By default, most keys manipulating the input (where you type your +messages and commands) behave like emacs does. + +Note that keys are case sensitive. Ctrl-X is not the same than Ctrl-x + +Key bindings listing +-------------------- +Some key bindings are available only in some tabs, others are global. + +Global keys +~~~~~~~~~~~ +These keys work in *any* tab. + +*Ctrl-n*:: Go to the next tab. + +*Ctrl-p*:: Go to the previous tab. + +*Alt-number*:: Go to tab number x. + +*Alt-j*:: Waits for you to type a two-digits number. Go to tab number xx. + +Input keys +~~~~~~~~~~ +These keys concern only the inputs. + +*Ctrl-a*:: Move the cursor to the beginning of line. + +*Ctrl-e*:: Move the cursor to the end of line. + +Chat tab input keys +~~~~~~~~~~~~~~~~~~~~~ +These keys work in any conversation tab (MultiUserChat, Private or Conversation tabs) + +*Key Up*:: Use the previous message from the message history. + +*Key Down*:: Use the next message from the message history. + +*Page Up*:: Scroll up in the conversation by x lines, where x is the height of the conversation window - 1. + +*Page Down*:: Likfe Page Up, but down. + +*Alt-/*:: Complete what you’re typing using the "recent" words from the current conversation, if any. + +Key configuration +----------------- +Bindings are keyboard shortcut aliases. You can use them +to define your own keys to replace the default ones. +where ^x means Control + x +and M-x means Alt + x + +To know exactly what the code of a key is, just run +================== +python3 src/keyboard.py +================== +And enter any keys + +.Turn Alt-i into a tab key (completion, etc) +================== +M-i = ^I +================== diff --git a/doc/en/themes.txt b/doc/en/themes.txt new file mode 100644 index 00000000..ebf654fe --- /dev/null +++ b/doc/en/themes.txt @@ -0,0 +1,89 @@ +Themes +====== + +This page describes how themes work in poezio and how to create or +modify one. + +A theme contains color attributes and character definitions. Poezio can display +up to _256_ colors if your terminal supports it. Most of the time, +if it doesn’t work, that’s because the _$TERM_ environnment variable is +wrong. For example with tmux or screen, set it to _screen-256color_, in +_xterm_, set it to _xterm-256color_, etc. If your terminal doesn’t have 256, +only 8 color will be available, and poezio will replace the colors by one +of the 8 values available. Thus, some theme file may not work properly +if you only have 8 colors for example light gray on dark gray may +be converted to black on black if only 8 colors are available, making +the text impossible to read). The default theme should work properly in any +case. If not, that’s a bug. + +A theme file is a python file (with the .py extension) containing a +class, inheriting the *themimg.Theme* class defined into the *theming* +poezio module. + +Create a theme +-------------- + +To create a theme named foo, create a file named foo.py into the theme +directory (by default it’s _~/.local/share/poezio/themes/_) and insert +into it: + +[source,python] +---- +import theming + +class FooTheme(theming.Theme): + # Define here colors for that theme +theme = FooTheme() +---- + +To define a _color pair_ and assign it to the COLOR_NAME option, just do +[source,python] +---- +class FooTheme(theming.Theme): + COLOR_NAME = (fg_color, bg_color, opt_attr) +---- + +You do not have to define all the <<available-options,available options>>, +you can decide that your theme will only change some options, the other +one will just have the default value (from the default theme). + +Colors and attributes +~~~~~~~~~~~~~~~~~~~~~ +A color pair defines how the text will be displayed on the screen. It +has a _foreground color_ (fg_color), a _background color_ (bg_color) +and an *_optional_* _attribute_ (opt_attr). + +Colors +^^^^^^ +A color is a number between -1 and 255. If it -1, this is the default +color defined by your terminal (for example if your terminal displays +text white on black by default, a fg_color of -1 is white, and a bg_color +of -1 is black). If it’s between 0 and 256 it represents one of the colors +on the image: + +image::../images/theme_256_colors.png["The list of all 256 colors", title="The list of all 256 colors"] + +Attributes +^^^^^^^^^^ +An attribute is a python string (so, it has to be surrounded by +*" "* or *' '*). It can be one of the following + +* *'b'*: bold text +* *'u'*: underlined text + +Use a theme +----------- +To use a theme, just define the _theme_ option into the +link:configure.html[configuration file] to the name of the theme you want +to use. If that theme is not found, the default theme will be used instead. +Note that the default theme is defined directly into poezio’s source code, +and note in a theme file. + +[[available-options]] +Available options +----------------- + +CAUTION: This section is not complete. + +All available options can be found into the default theme, which is into the +_theming.py_ file from the poezio’s source code. diff --git a/doc/images/theme_256_colors.png b/doc/images/theme_256_colors.png Binary files differnew file mode 100644 index 00000000..00e6c51d --- /dev/null +++ b/doc/images/theme_256_colors.png diff --git a/doc/poezio.txt b/doc/poezio.txt new file mode 100644 index 00000000..47497434 --- /dev/null +++ b/doc/poezio.txt @@ -0,0 +1,83 @@ +Poezio documentation +==================== + +This page is the documentation for poezio. + +Poezio is an XMPP console client mostly written in python and a little +bit in C. + +It uses curses to draw its user interface. + +It has been written to create an XMPP client that could very easily be used by +any IRC user. Its interface tries to be like the ones of famous clients such +as irssi or weechat. + +:numbered: +== Usage == + +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. + + +=== Commands === + +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 command nick with only one argument +========================================== +/nick "my new nick" +========================================== + +.The command status with two arguments +========================================== +/status away "on vacation" +========================================== + +.Note +The character *'* cannot be used instead of *"*. + + +To know the list of all available commands, use the *help* command with no +argument. To know more about the command (what it does and how to use it), +use the *help* command and pass the command name as its first argument. + +The list of all global commands is as follow: + +[horizontal] +*help*:: [command_name] + + Displays the list of all available commands in the current tab, or displays + the usage of the given command. +*message*:: <jid> [message] + + Open a conversation with the specified JID, and send a message to it, + if specified. + + + + + +.Get information on the status command +========================================== +/help status +========================================== + + +=== Tabs === +This section lists and describes all the tab types. + + +==== Roster Tab ==== +This is the first tab that you will see when starting poezio. + +It contains your roster + +[glossary] +== Glossary == + +This glossary explains some terms that are used in this documentation. + +[glossary] +Roster:: + The list of contacts, sorted by groups, status, or anything the client wishes. diff --git a/src/core.py b/src/core.py index 88a726ea..2176f43d 100644 --- a/src/core.py +++ b/src/core.py @@ -43,7 +43,6 @@ from data_forms import DataFormsTab from config import config, options from logger import logger from user import User -from room import Room from roster import Roster, RosterGroup, roster from contact import Contact, Resource from text_buffer import TextBuffer @@ -299,6 +298,9 @@ class Core(object): return self.information_buffer def grow_information_win(self, nb=1): + if self.information_win_size >= self.current_tab().height -5 or \ + self.information_win_size+nb >= self.current_tab().height-4: + return if self.information_win_size == 14: return self.information_win_size += nb @@ -401,10 +403,10 @@ class Core(object): nick = message['mucnick'] room_from = message.getMucroom() 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): - tab.get_room().get_user_by_name(nick).chatstate = state + if tab and tab.get_user_by_name(nick): + tab.get_user_by_name(nick).chatstate = state if tab == self.current_tab(): - tab.user_win.refresh(tab._room.users) + tab.user_win.refresh(tab.users) tab.input.refresh() self.doupdate() @@ -423,7 +425,6 @@ class Core(object): logger.log_roster_change(jid.bare, 'got offline') if not contact: return - log.debug('on_got_offline: %s' % presence) resource = contact.get_resource_by_fulljid(jid.full) if not resource: return @@ -469,7 +470,7 @@ class Core(object): """ tab = self.get_tab_by_name(jid, tabs.ConversationTab) if tab: - self.add_message_to_text_buffer(tab.get_room(), msg) + self.add_message_to_text_buffer(tab._text_buffer, msg) def on_failed_connection(self): """ @@ -483,7 +484,7 @@ class Core(object): """ for tab in self.tabs: if isinstance(tab, tabs.MucTab): - tab.get_room().disconnect() + tab.disconnect() self.information(_("Disconnected from server.")) def on_failed_auth(self, event): @@ -584,7 +585,7 @@ class Core(object): def on_user_changed_status_in_private(self, jid, msg): tab = self.get_tab_by_name(jid) if tab: # display the message in private - tab.get_room().add_message(msg) + tab.add_message(msg) def on_message(self, message): """ @@ -610,16 +611,16 @@ class Core(object): jid = message['from'] nick_from = jid.resource room_from = jid.bare - room = self.get_room_by_name(jid.full) # get the tab with the private conversation - if not room: # It's the first message we receive: create the tab - room = self.open_private_window(room_from, nick_from, False) - if not room: + tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation + if not tab: # It's the first message we receive: create the tab + tab = self.open_private_window(room_from, nick_from, False) + if not tab: return body = xhtml.get_body_from_message_stanza(message) if not body: return - room.add_message(body, time=None, nickname=nick_from, - forced_user=self.get_room_by_name(room_from).get_user_by_name(nick_from)) + tab.add_message(body, time=None, nickname=nick_from, + forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from)) conversation = self.get_tab_by_name(jid.full, tabs.PrivateTab) if conversation and conversation.remote_wants_chatstates is None: if message['chat_state']: @@ -673,7 +674,7 @@ class Core(object): remote_nick = roster.get_contact_by_jid(jid.bare).get_name() or jid.user else: remote_nick = jid.user - conversation.get_room().add_message(body, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER) + conversation._text_buffer.add_message(body, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER) if conversation.remote_wants_chatstates is None: if message['chat_state']: conversation.remote_wants_chatstates = True @@ -683,7 +684,7 @@ class Core(object): if 'private' in config.get('beep_on', 'highlight private').split(): curses.beep() if self.current_tab() is not conversation: - conversation.set_color_state(get_theme().COLOR_TAB_PRIVATE) + conversation.state = 'private' self.refresh_tab_win() else: self.refresh_window() @@ -745,7 +746,7 @@ class Core(object): roster.add_contact(contact, jid) roster.edit_groups_of_contact(contact, []) contact.set_ask('asked') - self.get_tab_by_number(0).set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) + self.get_tab_by_number(0).state = 'highlight' self.information('%s wants to subscribe to your presence'%jid, 'Roster') if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() @@ -833,7 +834,7 @@ class Core(object): for tab in self.tabs: if isinstance(tab, tabs.ConversationTab): if tab.get_name() == jid: - return tab.get_room() + return tab return None def get_tab_by_name(self, name, typ=None): @@ -854,16 +855,6 @@ class Core(object): return tab return None - def get_room_by_name(self, name): - """ - returns the room that has this name - """ - for tab in self.tabs: - if (isinstance(tab, tabs.MucTab) or - isinstance(tab, tabs.PrivateTab)) and tab.get_name() == name: - return tab.get_room() - return None - def init_curses(self, stdscr): """ ncurses initialization @@ -896,7 +887,7 @@ class Core(object): """ Refresh everything """ - self.current_tab().set_color_state(get_theme().COLOR_TAB_CURRENT) + self.current_tab().state = 'current' self.current_tab().refresh() self.doupdate() @@ -925,8 +916,7 @@ class Core(object): """ Open a new tab.MucTab containing a muc Room, using the specified nick """ - r = Room(room, nick) - new_tab = tabs.MucTab(r) + new_tab = tabs.MucTab(room, nick) self.add_tab(new_tab, focus) self.refresh_window() @@ -944,19 +934,19 @@ class Core(object): - A Muc with any new message """ for tab in self.tabs: - if tab.get_color_state() == get_theme().COLOR_TAB_PRIVATE: + if tab.state == 'private': self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == get_theme().COLOR_TAB_HIGHLIGHT: + if tab.state == 'highlight': self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == get_theme().COLOR_TAB_NEW_MESSAGE: + if tab.state == 'message': self.command_win('%s' % tab.nb) return for tab in self.tabs: - if tab.get_color_state() == get_theme().COLOR_TAB_DISCONNECTED: + if tab.state == 'disconnected': self.command_win('%s' % tab.nb) return for tab in self.tabs: @@ -1013,20 +1003,20 @@ class Core(object): def room_error(self, error, room_name): """ - Display the error on the room window + Display the error in the tab """ - room = self.get_room_by_name(room_name) + tab = self.get_tab_by_name(room_name) error_message = self.get_error_message_from_error_stanza(error) - self.add_message_to_text_buffer(room, error_message) + self.add_message_to_text_buffer(tab._text_buffer, error_message) code = error['error']['code'] if code == '401': msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') - self.add_message_to_text_buffer(room, msg) + self.add_message_to_text_buffer(tab._text_buffer, msg) if code == '409': if config.get('alternative_nickname', '') != '': - self.command_join('%s/%s'% (room.name, room.own_nick+config.get('alternative_nickname', ''))) + self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', ''))) else: - self.add_message_to_text_buffer(room, _('You can join the room with an other nick, by typing "/join /other_nick"')) + self.add_message_to_text_buffer(tab._text_buffer, _('You can join the room with an other nick, by typing "/join /other_nick"')) self.refresh_window() def open_conversation_window(self, jid, focus=True): @@ -1040,6 +1030,8 @@ class Core(object): return tab new_tab = tabs.ConversationTab(jid) # insert it in the rooms + if not focus: + new_tab.state = "private" self.add_tab(new_tab, focus) self.refresh_window() return new_tab @@ -1050,19 +1042,18 @@ class Core(object): if isinstance(tab, tabs.PrivateTab): if tab.get_name() == complete_jid: self.command_win('%s' % tab.nb) - return tab.get_room() + return tab # create the new tab - room = self.get_room_by_name(room_name) - if not room: + tab = self.get_tab_by_name(room_name, tabs.MucTab) + if not tab: return None - own_nick = room.own_nick - r = Room(complete_jid, own_nick) # PrivateRoom here - new_tab = tabs.PrivateTab(r) + new_tab = tabs.PrivateTab(complete_jid, tab.own_nick) + if not focus: + new_tab.state = "private" # insert it in the tabs self.add_tab(new_tab, focus) - # self.window.new_room(r) self.refresh_window() - return r + return new_tab def on_groupchat_subject(self, message): """ @@ -1070,15 +1061,15 @@ class Core(object): """ nick_from = message['mucnick'] room_from = message.getMucroom() - room = self.get_room_by_name(room_from) + tab = self.get_tab_by_name(room_from, tabs.MucTab) subject = message['subject'] - if not subject or not room: + if not subject or not tab: return if nick_from: - self.add_message_to_text_buffer(room, _("%(nick)s set the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=None) + self.add_message_to_text_buffer(tab._text_buffer, _("%(nick)s set the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=None) else: - self.add_message_to_text_buffer(room, _("The subject is: %(subject)s") % {'subject':subject}, time=None) - room.topic = subject + self.add_message_to_text_buffer(tab._text_buffer, _("The subject is: %(subject)s") % {'subject':subject}, time=None) + tab.topic = subject if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): self.refresh_window() @@ -1106,34 +1097,33 @@ class Core(object): room_from = message.getMucroom() if message['type'] == 'error': # Check if it's an error 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\ - tab.get_room().get_user_by_name(nick_from) in tab.ignores: - return - if not room: + if not tab: self.information(_("message received for a non-existing room: %s") % (room_from)) return + if tab.get_user_by_name(nick_from) and\ + tab.get_user_by_name(nick_from) in tab.ignores: + return body = xhtml.get_body_from_message_stanza(message) if body: date = date if delayed == True else None - self.add_message_to_text_buffer(room, body, date, nick_from, history=True if date else False) + tab.add_message(body, date, nick_from, history=True if date else False) if tab is self.current_tab(): - tab.text_win.refresh(tab._room) - tab.info_header.refresh(tab._room, tab.text_win) + tab.text_win.refresh() + tab.info_header.refresh(tab, tab.text_win) self.refresh_tab_win() if 'message' in config.get('beep_on', 'highlight private').split(): curses.beep() - def add_message_to_text_buffer(self, room, txt, time=None, nickname=None, history=None): + def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None): """ Add the message to the room if possible, else, add it to the Info window (in the Info tab of the info window in the RosterTab) """ - if not room: + if not buff: self.information('Trying to add a message in no room: %s' % txt, 'Error') else: - room.add_message(txt, time, nickname, history=history) + buff.add_message(txt, time, nickname, history=history) def command_help(self, arg): """ @@ -1183,12 +1173,14 @@ class Core(object): pres['type'] = show pres.send() current = self.current_tab() - if isinstance(current, tabs.MucTab) and current.get_room().joined: + if isinstance(current, tabs.MucTab) and current.joined and show in ('away', 'xa'): current.send_chat_state('inactive') for tab in self.tabs: - if isinstance(tab, tabs.MucTab) and tab.get_room().joined: - muc.change_show(self.xmpp, tab.get_room().name, tab.get_room().own_nick, show, msg) + if isinstance(tab, tabs.MucTab) and tab.joined: + muc.change_show(self.xmpp, tab.name, tab.own_nick, show, msg) self.set_status(show, msg) + if isinstance(current, tabs.MucTab) and current.joined and show not in ('away', 'xa'): + current.send_chat_state('active') def completion_status(self, the_input): return the_input.auto_completion([status for status in possible_show], ' ') @@ -1363,11 +1355,11 @@ class Core(object): args = common.shell_split(arg) password = None if len(args) == 0: - t = self.current_tab() - if not isinstance(t, tabs.MucTab) and not isinstance(t, tabs.PrivateTab): + tab = self.current_tab() + if not isinstance(tab, tabs.MucTab) and not isinstance(tab, tabs.PrivateTab): return - room = JID(t.get_name()).bare - nick = t.get_room().own_nick + room = JID(tab.get_name()).bare + nick = tab.own_nick else: info = JID(args[0]) if info.resource == '': @@ -1378,12 +1370,12 @@ class Core(object): else: nick = info.resource if info.bare == '': # happens with /join /nickname, which is OK - t = self.current_tab() - if not isinstance(t, tabs.MucTab): + tab = self.current_tab() + if not isinstance(tab, tabs.MucTab): return - room = t.get_name() + room = tab.get_name() if nick == '': - nick = t.get_room().own_nick + nick = tab.own_nick else: room = info.bare if room.find('@') == -1: # no server is provided, like "/join hello" @@ -1396,25 +1388,25 @@ class Core(object): self.information(_("You didn't specify a server for the room you want to join"), 'Error') return room = room.lower() - r = self.get_room_by_name(room) + tab = self.get_tab_by_name(room, tabs.MucTab) if len(args) == 2: # a password is provided password = args[1] - if r and r.joined: # if we are already in the room - self.focus_tab_named(r.name) + if tab and tab.joined: # if we are already in the room + self.focus_tab_named(tab.name) return if room.startswith('@'): room = room[1:] current_status = self.get_status() - if r and not r.joined: + if tab and not tab.joined: muc.join_groupchat(self.xmpp, room, nick, password, histo_length, current_status.message, current_status.show) - if not r: # if the room window exists, we don't recreate it. + if not tab: self.open_new_room(room, nick) muc.join_groupchat(self.xmpp, room, nick, password, histo_length, current_status.message, current_status.show) else: - r.own_nick = nick - r.users = [] + tab.own_nick = nick + tab.users = [] self.enable_private_tabs(room) def get_bookmark_nickname(self, room_name): @@ -1438,10 +1430,10 @@ class Core(object): if len(args) == 0 and not isinstance(self.current_tab(), tabs.MucTab): return if len(args) == 0: - room = self.current_tab().get_room() - roomname = self.current_tab().get_name() - if room.joined: - nick = room.own_nick + tab = self.current_tab() + roomname = tab.get_name() + if tab.joined: + nick = tab.own_nick else: info = JID(args[0]) if info.resource != '': @@ -1541,9 +1533,9 @@ class Core(object): return for tab in self.tabs: if isinstance(tab, tabs.MucTab) and JID(tab.get_name()).domain == domain: - if tab.get_room().joined: - muc.leave_groupchat(tab.core.xmpp, tab.get_name(), tab.get_room().own_nick, message) - tab.get_room().joined = False + if tab.joined: + muc.leave_groupchat(tab.core.xmpp, tab.get_name(), tab.own_nick, message) + tab.joined = False self.command_join(tab.get_name()) def command_bind(self, arg): @@ -1600,7 +1592,7 @@ class Core(object): self.pop_information_win_up(nb_lines, popup_time) else: if self.information_win_size != 0: - self.information_win.refresh(self.information_buffer) + self.information_win.refresh() self.current_tab().input.refresh() def disconnect(self, msg=None, reconnect=False): @@ -1610,7 +1602,7 @@ class Core(object): """ for tab in self.tabs: if isinstance(tab, tabs.MucTab): - muc.leave_groupchat(self.xmpp, tab.get_room().name, tab.get_room().own_nick, msg) + muc.leave_groupchat(self.xmpp, tab.name, tab.own_nick, msg) roster.empty() self.save_config() # Ugly fix thanks to gmail servers @@ -1659,7 +1651,6 @@ class Core(object): def remove_timed_event(self, event): if event and event in self.timed_events: - log.debug('removing event') self.timed_events.remove(event) def add_timed_event(self, event): diff --git a/src/room.py b/src/room.py deleted file mode 100644 index b97dd0b6..00000000 --- a/src/room.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2010-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. - -from text_buffer import TextBuffer, Message -from datetime import datetime -from random import randrange -from config import config -from logger import logger - -import common -from theming import get_theme - -import logging -import curses - -log = logging.getLogger(__name__) - -class Room(TextBuffer): - def __init__(self, name, nick, messages_nb_limit=config.get('max_messages_in_memory', 2048)): - TextBuffer.__init__(self, messages_nb_limit) - self.name = name - self.own_nick = nick - self.color_state = get_theme().COLOR_TAB_NORMAL # color used in RoomInfo - self.joined = False # false until self presence is receied - self.users = [] # User objects - self.topic = '' - - def disconnect(self): - """ - Set the state of the room as not joined, so - we can know if we can join it, send messages to it, etc - """ - self.users = [] - self.color_state = get_theme().COLOR_TAB_DISCONNECTED - self.joined = False - - def get_single_line_topic(self): - """ - Return the topic as a single-line string (for the window header) - """ - return self.topic.replace('\n', '|') - - def log_message(self, txt, time, nickname): - """ - Log the messages in the archives, if it needs - to be - """ - if time is None and self.joined: # don't log the history messages - logger.log_message(self.name, nickname, txt) - - def do_highlight(self, txt, time, nickname): - """ - Set the tab color and returns the nick color - """ - color = None - if not time and nickname and nickname != self.own_nick and self.joined: - if self.own_nick.lower() in txt.lower(): - if self.color_state != get_theme().COLOR_TAB_CURRENT: - self.set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) - color = get_theme().COLOR_HIGHLIGHT_NICK - else: - highlight_words = config.get('highlight_on', '').split(':') - for word in highlight_words: - if word and word.lower() in txt.lower(): - if self.color_state != get_theme().COLOR_TAB_CURRENT: - self.set_color_state(get_theme().COLOR_TAB_HIGHLIGHT) - color = get_theme().COLOR_HIGHLIGHT_NICK - break - if color: - beep_on = config.get('beep_on', 'highlight private').split() - if 'highlight' in beep_on and 'message' not in beep_on: - curses.beep() - return color - - def get_user_by_name(self, nick): - """ - Gets the user associated with the given nick, or None if not found - """ - for user in self.users: - if user.nick == nick: - return user - return None - - def set_color_state(self, color): - """ - Set the color that will be used to display the room's - number in the RoomInfo window - """ - self.color_state = color - - def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, history=None): - """ - Note that user can be None even if nickname is not None. It happens - when we receive an history message said by someone who is not - in the room anymore - """ - self.log_message(txt, time, nickname) - special_message = False - if txt.startswith('/me '): - txt = "\x192}* \x195}" + nickname + ' ' + txt[4:] - special_message = True - user = self.get_user_by_name(nickname) if nickname is not None else None - if user: - user.set_last_talked(datetime.now()) - if not user and forced_user: - user = forced_user - if not time and nickname and\ - nickname != self.own_nick and\ - self.color_state != get_theme().COLOR_TAB_CURRENT: - if self.color_state != get_theme().COLOR_TAB_HIGHLIGHT: - self.set_color_state(get_theme().COLOR_TAB_NEW_MESSAGE) - nick_color = nick_color or None - if not nickname or time: - txt = '\x195}%s' % (txt,) - else: # TODO - highlight = self.do_highlight(txt, time, nickname) - if highlight: - nick_color = highlight - if special_message: - txt = '\x195}%s' % (txt,) - nickname = None - time = time or datetime.now() - message = Message(txt='%s\x19o'%(txt.replace('\t', ' '),), nick_color=nick_color, - time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\ - if history else time.strftime("%H:%M:%S"),\ - nickname=nickname, user=user) - while len(self.messages) > self.messages_nb_limit: - self.messages.pop(0) - self.messages.append(message) - for window in self.windows: # make the associated windows - # build the lines from the new message - nb = window.build_new_message(message, history=history) - if window.pos != 0: - window.scroll_up(nb) - return nb diff --git a/src/tabs.py b/src/tabs.py index b0de89c0..c18f6d76 100644 --- a/src/tabs.py +++ b/src/tabs.py @@ -13,8 +13,8 @@ Each Tab object has different refresh() and resize() methods, defining how its Windows are displayed, resized, etc """ -MIN_WIDTH = 50 -MIN_HEIGHT = 22 +MIN_WIDTH = 42 +MIN_HEIGHT = 6 import logging log = logging.getLogger(__name__) @@ -41,6 +41,7 @@ from sleekxmpp.xmlstream.stanzabase import JID from config import config from roster import RosterGroup, roster from contact import Contact, Resource +from text_buffer import TextBuffer from user import User from os import getenv, path from logger import logger @@ -57,12 +58,32 @@ SHOW_NAME = { NS_MUC_USER = 'http://jabber.org/protocol/muc#user' +STATE_COLORS = { + 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED, + 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE, + 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT, + 'private': lambda: get_theme().COLOR_TAB_PRIVATE, + 'normal': lambda: get_theme().COLOR_TAB_NORMAL, + 'current': lambda: get_theme().COLOR_TAB_CURRENT, +# 'attention': lambda: get_theme().COLOR_TAB_ATTENTION, + } + +STATE_PRIORITY = { + 'normal': -1, + 'current': -1, + 'disconnected': 0, + 'message': 1, + 'highlight': 2, + 'private': 2, +# 'attention': 3 + } + class Tab(object): number = 0 tab_core = None def __init__(self): self.input = None - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' self.need_resize = False self.nb = Tab.number Tab.number += 1 @@ -87,6 +108,25 @@ class Tab(object): def info_win(self): return self.core.information_win + @property + def color(self): + return STATE_COLORS[self._state]() + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + if not value in STATE_COLORS: + log.debug("WARNING: invalid value for tab state") + return + elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \ + value != 'current': + log.debug("WARNING: did not set status because of lower priority") + return + self._state = value + @staticmethod def resize(scr): Tab.size = (Tab.height, Tab.width) = scr.getmaxyx() @@ -167,18 +207,6 @@ class Tab(object): """ raise NotImplementedError - def get_color_state(self): - """ - returns the color that should be used in the GlobalInfoBar - """ - return self._color_state - - def set_color_state(self, color): - """ - set the color state - """ - pass - def get_name(self): """ get the name of the tab @@ -198,22 +226,13 @@ class Tab(object): """ called when this tab loses the focus. """ - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' def on_gain_focus(self): """ called when this tab gains the focus. """ - self._color_state = get_theme().COLOR_TAB_CURRENT - - def add_message(self): - """ - Adds a message in the tab. - If the tab cannot add a message in itself (for example - FormTab, where text is not intented to be appened), it returns False. - If the tab can, it returns True - """ - return False + self._state = 'current' def on_scroll_down(self): """ @@ -258,9 +277,9 @@ class ChatTab(Tab): Also, ^M is already bound to on_enter And also, add the /say command """ - def __init__(self, room): + def __init__(self): Tab.__init__(self) - self._room = room + self._text_buffer = TextBuffer() self.remote_wants_chatstates = None # change this to True or False when # we know that the remote user wants chatstates, or not. # None means we don’t know yet, and we send only "active" chatstates @@ -286,7 +305,7 @@ class ChatTab(Tab): # build the list of the recent words char_we_dont_want = string.punctuation+' ' words = list() - for msg in self._room.messages[:-40:-1]: + for msg in self.messages[:-40:-1]: if not msg: continue txt = xhtml.clean_text(msg.txt) @@ -312,7 +331,7 @@ class ChatTab(Tab): """ Send an empty chatstate message """ - if not isinstance(self, MucTab) or self.get_room().joined: + if not isinstance(self, MucTab) or self.joined: if state in ('active', 'inactive', 'gone') and self.core.status.show in ('xa', 'away') and not always_send: return msg = self.core.xmpp.make_message(self.get_name()) @@ -371,7 +390,7 @@ class ChatTab(Tab): def move_separator(self): self.text_win.remove_line_separator() self.text_win.add_line_separator() - self.text_win.refresh(self._room) + self.text_win.refresh(self._text_buffer) self.input.refresh() def get_conversation_messages(self): @@ -386,15 +405,20 @@ class MucTab(ChatTab): It contains an userlist, an input, a topic, an information and a chat zone """ message_type = 'groupchat' - def __init__(self, room): - ChatTab.__init__(self, room) + def __init__(self, jid, nick): + ChatTab.__init__(self) + self.own_nick = nick + self.name = jid + self.joined = False + self.users = [] + self.topic = '' self.remote_wants_chatstates = True # We send active, composing and paused states to the MUC because # the chatstate may or may not be filtered by the MUC, # that’s not our problem. self.topic_win = windows.Topic() self.text_win = windows.TextWin() - room.add_window(self.text_win) + self._text_buffer.add_window(self.text_win) self.v_separator = windows.VerticalSeparator() self.user_win = windows.UserList() self.info_header = windows.MucInfoWin() @@ -413,6 +437,7 @@ class MucTab(ChatTab): 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) @@ -426,19 +451,19 @@ class MucTab(ChatTab): def scroll_user_list_up(self): self.user_win.scroll_up() - self.user_win.refresh(self._room.users) + self.user_win.refresh(self.users) self.input.refresh() def scroll_user_list_down(self): self.user_win.scroll_down() - self.user_win.refresh(self._room.users) + self.user_win.refresh(self.users) self.input.refresh() def command_info(self, arg): args = common.shell_split(arg) if len(args) != 1: return self.core.information("Info command takes only 1 argument") - user = self.get_room().get_user_by_name(args[0]) + user = self.get_user_by_name(args[0]) if not user: return self.core.information("Unknown user: %s" % args[0]) info = '%s%s: show: %s, affiliation: %s, role: %s%s' % (args[0], @@ -473,35 +498,34 @@ class MucTab(ChatTab): """ /clear """ - self._room.messages = [] - self.text_win.rebuild_everything(self._room) + self.messages = [] + self.text_win.rebuild_everything(self._text_buffer) 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') + if self.joined: + muc.leave_groupchat(self.core.xmpp, self.get_name(), self.own_nick, arg) + self.disconnect() + self.core.disable_private_tabs(self.name) + self.core.command_join('"/%s"' % self.core.get_bookmark_nickname(self.name), '0') self.user_win.pos = 0 def command_recolor(self, arg): """ Re-assign color to the participants of the room """ - room = self.get_room() compare_users = lambda x: x.last_talked - users = list(room.users) + users = list(self.users) # search our own user, to remove it from the room for user in users: - if user.nick == room.own_nick: + if user.nick == self.own_nick: users.remove(user) nb_color = len(get_theme().LIST_COLOR_NICKNAMES) for i, user in enumerate(sorted(users, key=compare_users, reverse=True)): user.color = get_theme().LIST_COLOR_NICKNAMES[i % nb_color] - self.text_win.rebuild_everything(self._room) - self.text_win.refresh(self._room) + self.text_win.rebuild_everything(self._text_buffer) + self.text_win.refresh() self.input.refresh() def command_version(self, arg): @@ -520,8 +544,8 @@ class MucTab(ChatTab): args = common.shell_split(arg) if len(args) < 1: return - if args[0] in [user.nick for user in self.get_room().users]: - jid = self._room.name + '/' + args[0] + if args[0] in [user.nick for user in self.users]: + jid = self.name + '/' + args[0] else: jid = args[0] self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) @@ -534,26 +558,35 @@ class MucTab(ChatTab): if len(args) != 1: return nick = args[0] - room = self.get_room() - if not room.joined: + if not self.joined: return current_status = self.core.get_status() - muc.change_nick(self.core.xmpp, room.name, nick, current_status.message, current_status.show) + muc.change_nick(self.core.xmpp, self.name, nick, current_status.message, current_status.show) def command_part(self, arg): """ /part [msg] """ args = arg.split() - room = self.get_room() if len(args): msg = ' '.join(args) else: msg = None - if self.get_room().joined: - muc.leave_groupchat(self.core.xmpp, room.name, room.own_nick, arg) + if self.joined: + muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg) + self.joined = False + self.add_message(_("\x195}You left the chatroom\x193}")) + self.refresh() + self.core.doupdate() + self.core.disable_private_tabs(self.name) + + def command_close(self, arg): + """ + /close [msg] + """ + self.command_part(arg) self.core.close_tab() - self.core.disable_private_tabs(self.get_room().name) + def command_query(self, arg): """ @@ -563,11 +596,10 @@ class MucTab(ChatTab): if len(args) < 1: return nick = args[0] - room = self.get_room() r = None - for user in room.users: + for user in self.users: if user.nick == nick: - r = self.core.open_private_window(room.name, user.nick) + r = self.core.open_private_window(self.name, user.nick) if r and len(args) > 1: msg = arg[len(nick)+1:] self.core.current_tab().command_say(msg) @@ -579,23 +611,22 @@ class MucTab(ChatTab): /topic [new topic] """ if not arg.strip(): - self.core.add_message_to_text_buffer(self.get_room(), - _("The subject of the room is: %s") % self.get_room().topic) - self.text_win.refresh(self.get_room()) + self._text_buffer.add_message_to_text_buffer(self, + _("The subject of the room is: %s") % self.topic) + self.text_win.refresh(self._text_buffer) self.input.refresh() return subject = arg - muc.change_subject(self.core.xmpp, self.get_room().name, subject) + muc.change_subject(self.core.xmpp, self.name, subject) def command_names(self, arg=None): """ /names """ - room = self.get_room() - if not room.joined: + if not self.joined: return users, visitors, moderators, participants, others = [], [], [], [], [] - for user in room.users: + for user in self.users: if user.role == 'visitor': visitors.append(user.nick) elif user.role == 'participant': @@ -617,12 +648,13 @@ class MucTab(ChatTab): message += '%s, ' % item message += '%s\n' % last - self.core.add_message_to_text_buffer(room, message) - self.text_win.refresh(self.get_room()) + # self.core.add_message_to_text_buffer(room, message) + self._text_buffer.add_message(message) + self.text_win.refresh() self.input.refresh() def completion_topic(self, the_input): - current_topic = self.get_room().topic + current_topic = self.topic return the_input.auto_completion([current_topic], '') def command_kick(self, arg): @@ -633,7 +665,7 @@ class MucTab(ChatTab): if not len(args): self.core.command_help('kick') else: - self.command_role('none '+arg) + self.command_role(arg+ ' none') def command_role(self, arg): """ @@ -650,7 +682,7 @@ class MucTab(ChatTab): reason = ' '.join(args[2:]) else: reason = '' - if not self.get_room().joined or \ + if not self.joined or \ not role in ('none', 'visitor', 'participant', 'moderator'): return res = muc.set_user_role(self.core.xmpp, self.get_name(), nick, reason, role) @@ -672,7 +704,7 @@ class MucTab(ChatTab): reason = ' '.join(args[2:]) else: reason = '' - if not self.get_room().joined or \ + if not self.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'): @@ -705,7 +737,7 @@ class MucTab(ChatTab): self.core.command_help('ignore') return nick = args[0] - user = self._room.get_user_by_name(nick) + user = self.get_user_by_name(nick) if not user: self.core.information(_('%s is not in the room') % nick) elif user in self.ignores: @@ -723,7 +755,7 @@ class MucTab(ChatTab): self.core.command_help('unignore') return nick = args[0] - user = self._room.get_user_by_name(nick) + user = self.get_user_by_name(nick) if not user: self.core.information(_('%s is not in the room') % nick) elif user not in self.ignores: @@ -746,7 +778,7 @@ class MucTab(ChatTab): self.topic_win.resize(1, self.width, 0, 0) self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10)) self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0) - self.text_win.rebuild_everything(self._room) + self.text_win.rebuild_everything(self._text_buffer) self.user_win.resize(self.height-3-self.core.information_win_size-1, self.width-text_width-1, 1, text_width+1) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) self.input.resize(1, self.width, self.height-1, 0) @@ -755,13 +787,13 @@ class MucTab(ChatTab): if self.need_resize: self.resize() log.debug(' TAB Refresh: %s'%self.__class__.__name__) - self.topic_win.refresh(self._room.get_single_line_topic()) - self.text_win.refresh(self._room) + self.topic_win.refresh(self.get_single_line_topic()) + self.text_win.refresh() self.v_separator.refresh() - self.user_win.refresh(self._room.users) - self.info_header.refresh(self._room, self.text_win) + self.user_win.refresh(self.users) + self.info_header.refresh(self, self.text_win) self.tab_win.refresh() - self.info_win.refresh(self.core.informations) + self.info_win.refresh() self.input.refresh() def on_input(self, key): @@ -782,8 +814,8 @@ class MucTab(ChatTab): # If we are not completing a command or a command's argument, complete a nick compare_users = lambda x: x.last_talked - word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)\ - if user.nick != self._room.own_nick] + word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\ + if user.nick != self.own_nick] after = config.get('after_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\ @@ -796,33 +828,38 @@ class MucTab(ChatTab): self.send_composing_chat_state(empty_after) def get_color_state(self): - return self._room.color_state + return self.color_state def set_color_state(self, color): - self._room.set_color_state(color) + self.set_color_state(color) def get_name(self): - return self._room.name + return self.name def get_text_window(self): return self.text_win - def get_room(self): - return self._room + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + self._state = value def on_lose_focus(self): - self._room.set_color_state(get_theme().COLOR_TAB_NORMAL) + self._state = 'normal' self.text_win.remove_line_separator() self.text_win.add_line_separator() if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('inactive') def on_gain_focus(self): - self._room.set_color_state(get_theme().COLOR_TAB_CURRENT) + self._state = 'current' if self.text_win.built_lines and self.text_win.built_lines[-1] is None: self.text_win.remove_line_separator() curses.curs_set(1) - if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): + if self.joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('active') def on_scroll_up(self): @@ -852,88 +889,89 @@ class MucTab(ChatTab): role = presence['muc']['role'] jid = presence['muc']['jid'] typ = presence['type'] - room = self.get_room() - if not room.joined: # user in the room BEFORE us. + if not self.joined: # user in the room BEFORE us. # ignore redondant presence message, see bug #1509 - if from_nick not in [user.nick for user in room.users] and typ != "unavailable": + if from_nick not in [user.nick for user in self.users] and typ != "unavailable": new_user = User(from_nick, affiliation, show, status, role, jid) - room.users.append(new_user) - if from_nick == room.own_nick: - room.joined = True + self.users.append(new_user) + if from_nick == self.own_nick: + self.joined = True if self.core.current_tab() == self and self.core.status.show not in ('xa', 'away'): self.send_chat_state('active') new_user.color = get_theme().COLOR_OWN_NICK - room.add_message(_("\x195}Your nickname is \x193}%s") % (from_nick)) + self.add_message(_("\x195}Your nickname is \x193}%s") % (from_nick)) if '170' in status_codes: - room.add_message('\x191}Warning: \x195}this room is publicly logged') + self.add_message('\x191}Warning: \x195}this room is publicly logged') else: change_nick = '303' in status_codes kick = '307' in status_codes and typ == 'unavailable' ban = '301' in status_codes and typ == 'unavailable' - user = room.get_user_by_name(from_nick) + user = self.get_user_by_name(from_nick) # New user if not user: - self.on_user_join(room, from_nick, affiliation, show, status, role, jid) + self.on_user_join(from_nick, affiliation, show, status, role, jid) # nick change elif change_nick: - self.on_user_nick_change(room, presence, user, from_nick, from_room) + self.on_user_nick_change(presence, user, from_nick, from_room) elif ban: - self.on_user_banned(room, presence, user, from_nick) + self.on_user_banned(presence, user, from_nick) # kick elif kick: - self.on_user_kicked(room, presence, user, from_nick) + self.on_user_kicked(presence, user, from_nick) # user quit elif typ == 'unavailable': - self.on_user_leave_groupchat(room, user, jid, status, from_nick, from_room) + self.on_user_leave_groupchat(user, jid, status, from_nick, from_room) # status change else: - self.on_user_change_status(room, user, from_nick, from_room, affiliation, role, show, status) + self.on_user_change_status(user, from_nick, from_room, affiliation, role, show, status) if self.core.current_tab() is self: - self.text_win.refresh(self._room) - self.user_win.refresh(self._room.users) - self.info_header.refresh(self._room, self.text_win) + self.text_win.refresh() + self.user_win.refresh(self.users) + self.info_header.refresh(self, self.text_win) self.input.refresh() self.core.doupdate() - def on_user_join(self, room, from_nick, affiliation, show, status, role, jid): + def on_user_join(self, from_nick, affiliation, show, status, role, jid): """ When a new user joins the groupchat """ - room.users.append(User(from_nick, affiliation, - show, status, role, jid)) + user = User(from_nick, affiliation, + show, status, role, jid) + self.users.append(user) hide_exit_join = config.get('hide_exit_join', -1) if hide_exit_join != 0: + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 if not jid.full: - room.add_message('\x194}%(spec)s \x193}%(nick)s\x195} joined the room' % {'nick':from_nick, 'spec':get_theme().CHAR_JOIN}) + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN}) else: - room.add_message('\x194}%(spec)s \x193}%(nick)s \x195}(\x194}%(jid)s\x195}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'jid':jid.full}) + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s \x195}(\x194}%(jid)s\x195}) joined the room' % {'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full}) self.core.on_user_rejoined_private_conversation(room.name, from_nick) - - def on_user_nick_change(self, room, presence, user, from_nick, from_room): + def on_user_nick_change(self, presence, user, from_nick, from_room): new_nick = presence.find('{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick'] - if user.nick == room.own_nick: - room.own_nick = new_nick + if user.nick == self.own_nick: + self.own_nick = new_nick # also change our nick in all private discussion of this room for _tab in self.core.tabs: - if isinstance(_tab, PrivateTab) and JID(_tab.get_name()).bare == room.name: - _tab.get_room().own_nick = new_nick + if isinstance(_tab, PrivateTab) and JID(_tab.get_name()).bare == self.name: + _tab.own_nick = new_nick user.change_nick(new_nick) - room.add_message('\x193}%(old)s\x195} is now known as \x193}%(new)s' % {'old':from_nick, 'new':new_nick}) + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 + self.add_message('\x19%(color)d}%(old)s\x195} is now known as \x19%(color)d}%(new)s' % {'old':from_nick, 'new':new_nick, 'color':color}) # rename the private tabs if needed - self.core.rename_private_tabs(room.name, from_nick, new_nick) + self.core.rename_private_tabs(self.name, from_nick, new_nick) - def on_user_banned(self, room, presence, user, from_nick): + def on_user_banned(self, presence, user, from_nick): """ When someone is banned from a muc """ - room.users.remove(user) + self.users.remove(user) by = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) by = by.attrib['jid'] if by is not None else None - if from_nick == room.own_nick: # we are banned - room.disconnect() - self.core.disable_private_tabs(room.name) + if from_nick == self.own_nick: # we are banned + self.disconnect() + self.core.disable_private_tabs(self.name) self.tab_win.refresh() self.core.doupdate() if by: @@ -941,25 +979,26 @@ class MucTab(ChatTab): else: kick_msg = _('\x191}%(spec)s \x193}You\x195} have been banned.') % {'spec':get_theme().CHAR_KICK} else: + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 if by: - kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'by':by} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned by \x194}%(by)s') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by} else: - kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been banned') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color} if reason is not None and reason.text: kick_msg += _('\x195} Reason: \x196}%(reason)s\x195}') % {'reason': reason.text} - room.add_message(kick_msg) + self._text_buffer.add_message(kick_msg) - def on_user_kicked(self, room, presence, user, from_nick): + def on_user_kicked(self, presence, user, from_nick): """ When someone is kicked from a muc """ - room.users.remove(user) + self.users.remove(user) by = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER)) by = by.attrib['jid'] if by is not None else None - if from_nick == room.own_nick: # we are kicked - room.disconnect() - self.core.disable_private_tabs(room.name) + if from_nick == self.own_nick: # we are kicked + self.disconnect() + self.core.disable_private_tabs(self.name) self.tab_win.refresh() self.core.doupdate() if by: @@ -968,47 +1007,54 @@ class MucTab(ChatTab): kick_msg = _('\x191}%(spec)s \x193}You\x195} have been kicked.') % {'spec':get_theme().CHAR_KICK} # try to auto-rejoin if config.get('autorejoin', 'false') == 'true': - muc.join_groupchat(self.core.xmpp, room.name, room.own_nick) + muc.join_groupchat(self.core.xmpp, self.name, self.own_nick) else: + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 if by: - kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked by \x193}%(by)s') % {'spec':get_theme().CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'color':color, 'by':by.replace('"', '\\"')} else: - kick_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + kick_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has been kicked') % {'spec':get_theme().CHAR_KICK, 'nick':from_nick.replace('"', '\\"'), 'color':color} if reason is not None and reason.text: kick_msg += _('\x195} Reason: \x196}%(reason)s') % {'reason': reason.text} - room.add_message(kick_msg) + self.add_message(kick_msg) - def on_user_leave_groupchat(self, room, user, jid, status, from_nick, from_room): + def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room): """ When an user leaves a groupchat """ - room.users.remove(user) - if room.own_nick == user.nick: + self.users.remove(user) + if self.own_nick == user.nick: # We are now out of the room. Happens with some buggy (? not sure) servers - room.disconnect() + self.disconnect() self.core.disable_private_tabs(from_room) self.tab_win.refresh() self.core.doupdate() hide_exit_join = config.get('hide_exit_join', -1) if config.get('hide_exit_join', -1) >= -1 else -1 if hide_exit_join == -1 or user.has_talked_since(hide_exit_join): + log.debug("\n\nALLO: USERCOLOR: %s\n\n" % user.color.__repr__()) + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 if not jid.full: - leave_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT} + leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT} else: - leave_msg = _('\x191}%(spec)s \x193}%(nick)s\x195} (\x194}%(jid)s\x195}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'jid':jid.full} + leave_msg = _('\x191}%(spec)s \x19%(color)d}%(nick)s\x195} (\x194}%(jid)s\x195}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full} if status: leave_msg += ' (%s)' % status - room.add_message(leave_msg) + self.add_message(leave_msg) self.core.refresh_window() self.core.on_user_left_private_conversation(from_room, from_nick, status) - def on_user_change_status(self, room, user, from_nick, from_room, affiliation, role, show, status): + def on_user_change_status(self, user, from_nick, from_room, affiliation, role, show, status): """ When an user changes her status """ # build the message display_message = False # flag to know if something significant enough # to be displayed has changed - msg = _('\x193}%s\x195} changed: ')% from_nick.replace('"', '\\"') + color = user.color[0] if config.get('display_user_color_in_join_part', '') == 'true' else 3 + if from_nick == self.own_nick: + msg = _('\x193}You\x195} changed: ') + else: + msg = _('\x19%(color)d}%(nick)s\x195} changed: ') % {'nick': from_nick.replace('"', '\\"'), 'color': color} if show not in SHOW_NAME: self.core.information("%s from room %s sent an invalid show: %s" %\ (from_nick, from_room, show), "warning") @@ -1032,30 +1078,124 @@ class MucTab(ChatTab): if not display_message: return msg = msg[:-2] # remove the last ", " - hide_status_change = config.get('hide_status_change', -1) if config.get('hide_status_change', -1) >= -1 else -1 + hide_status_change = config.get('hide_status_change', -1) + if hide_status_change < -1: + hide_status_change = -1 if (hide_status_change == -1 or \ user.has_talked_since(hide_status_change) or\ - user.nick == room.own_nick)\ + user.nick == self.own_nick)\ and\ (affiliation != user.affiliation or\ role != user.role or\ show != user.show or\ status != user.status): # display the message in the room - room.add_message(msg) + self._text_buffer.add_message(msg) self.core.on_user_changed_status_in_private('%s/%s' % (from_room, from_nick), msg) # finally, effectively change the user status user.update(affiliation, show, status, role) + def disconnect(self): + """ + Set the state of the room as not joined, so + we can know if we can join it, send messages to it, etc + """ + self.users = [] + self._state = 'disconnected' + self.joined = False + + def get_single_line_topic(self): + """ + Return the topic as a single-line string (for the window header) + """ + return self.topic.replace('\n', '|') + + def log_message(self, txt, time, nickname): + """ + Log the messages in the archives, if it needs + to be + """ + if time is None and self.joined: # don't log the history messages + logger.log_message(self.name, nickname, txt) + + def do_highlight(self, txt, time, nickname): + """ + Set the tab color and returns the nick color + """ + color = None + if not time and nickname and nickname != self.own_nick and self.joined: + if self.own_nick.lower() in txt.lower(): + if self._state != 'current': + self._state = 'highlight' + color = get_theme().COLOR_HIGHLIGHT_NICK + else: + highlight_words = config.get('highlight_on', '').split(':') + for word in highlight_words: + if word and word.lower() in txt.lower(): + if self._state != 'current': + self._state = 'highlight' + color = get_theme().COLOR_HIGHLIGHT_NICK + break + if color: + beep_on = config.get('beep_on', 'highlight private').split() + if 'highlight' in beep_on and 'message' not in beep_on: + curses.beep() + return color + + def get_user_by_name(self, nick): + """ + Gets the user associated with the given nick, or None if not found + """ + for user in self.users: + if user.nick == nick: + return user + return None + + def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, history=None): + """ + Note that user can be None even if nickname is not None. It happens + when we receive an history message said by someone who is not + in the room anymore + """ + self.log_message(txt, time, nickname) + special_message = False + if txt.startswith('/me '): + txt = "\x192}* \x195}" + nickname + ' ' + txt[4:] + special_message = True + user = self.get_user_by_name(nickname) if nickname is not None else None + if user: + user.set_last_talked(datetime.now()) + if not user and forced_user: + user = forced_user + if not time and nickname and\ + nickname != self.own_nick and\ + self._state != 'current': + if self._state != 'highlight': + self._state = 'message' + nick_color = nick_color or None + if not nickname or time: + txt = '\x195}%s' % (txt,) + else: # TODO + highlight = self.do_highlight(txt, time, nickname) + if highlight: + nick_color = highlight + if special_message: + txt = '\x195}%s' % (txt,) + nickname = None + time = time or datetime.now() + self._text_buffer.add_message(txt, time, nickname, nick_color, history, user) + class PrivateTab(ChatTab): """ The tab containg a private conversation (someone from a MUC) """ message_type = 'chat' - def __init__(self, room): - ChatTab.__init__(self, room) + def __init__(self, name, nick): + ChatTab.__init__(self) + self.own_nick = nick + self.name = name self.text_win = windows.TextWin() - room.add_window(self.text_win) + self._text_buffer.add_window(self.text_win) self.info_header = windows.PrivateInfoWin() self.input = windows.MessageInput() # keys @@ -1063,10 +1203,10 @@ class PrivateTab(ChatTab): # 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['part'] = (self.command_unquery, _("Usage: /part\nPart: 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(room.name).bare, MucTab) + self.parent_muc = self.core.get_tab_by_name(JID(name).bare, MucTab) self.on = True def completion(self): @@ -1086,10 +1226,9 @@ class PrivateTab(ChatTab): needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active' msg['chat_state'] = needed msg.send() - self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick or self.get_room().own_nick) - logger.log_message(JID(self.get_name()).bare, self.core.own_nick, line) + self.core.add_message_to_text_buffer(self._text_buffer, line, None, self.core.own_nick or self.own_nick) self.cancel_paused_delay() - self.text_win.refresh(self._room) + self.text_win.refresh() self.input.refresh() def command_unquery(self, arg): @@ -1110,7 +1249,7 @@ class PrivateTab(ChatTab): res.get('version') or _('unknown'), res.get('os') or _('on an unknown platform')) self.core.information(version, 'Info') - jid = self.get_room().name + jid = self.name self.core.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) def command_info(self, arg): @@ -1120,7 +1259,7 @@ class PrivateTab(ChatTab): if arg: self.parent_muc.command_info(arg) else: - user = JID(self.get_room().name).resource + user = JID(self.name).resource self.parent_muc.command_info(user) def resize(self): @@ -1128,7 +1267,7 @@ class PrivateTab(ChatTab): return self.need_resize = False self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0) - self.text_win.rebuild_everything(self._room) + self.text_win.rebuild_everything(self._text_buffer) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) self.input.resize(1, self.width, self.height-1, 0) @@ -1136,27 +1275,26 @@ class PrivateTab(ChatTab): if self.need_resize: self.resize() log.debug(' TAB Refresh: %s'%self.__class__.__name__) - self.text_win.refresh(self._room) - self.info_header.refresh(self._room, self.text_win, self.chatstate) - self.info_win.refresh(self.core.informations) + self.text_win.refresh() + self.info_header.refresh(self.name, self.text_win, self.chatstate) + self.info_win.refresh() self.tab_win.refresh() self.input.refresh() def refresh_info_header(self): - self.info_header.refresh(self._room, self.text_win, self.chatstate) + self.info_header.refresh(self.name, self.text_win, self.chatstate) self.input.refresh() - def get_color_state(self): - if self._room.color_state == get_theme().COLOR_TAB_NORMAL or\ - self._room.color_state == get_theme().COLOR_TAB_CURRENT: - return self._room.color_state - return get_theme().COLOR_TAB_PRIVATE + @property + def state(self): + return self._state - def set_color_state(self, color): - self._room.color_state = color + @state.setter + def state(self, value): + self._state = value def get_name(self): - return self._room.name + return self.name def on_input(self, key): if key in self.key_func: @@ -1166,22 +1304,24 @@ class PrivateTab(ChatTab): if not self.on: return False empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - tab = self.core.get_tab_by_name(JID(self.get_room().name).bare, MucTab) - if tab and tab.get_room().joined: + tab = self.core.get_tab_by_name(JID(self.name).bare, MucTab) + if tab and tab.joined: self.send_composing_chat_state(empty_after) return False def on_lose_focus(self): - self._room.set_color_state(get_theme().COLOR_TAB_NORMAL) + self._state = 'normal' self.text_win.remove_line_separator() self.text_win.add_line_separator() - if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): + tab = self.core.get_tab_by_name(JID(self.name).bare, MucTab) + if tab.joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('inactive') def on_gain_focus(self): - self._room.set_color_state(get_theme().COLOR_TAB_CURRENT) + self._state = 'current' curses.curs_set(1) - if self.get_room().joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): + tab = self.core.get_tab_by_name(JID(self.name).bare, MucTab) + if tab.joined and config.get('send_chat_states', 'true') == 'true' and not self.input.get_text(): self.send_chat_state('active') def on_scroll_up(self): @@ -1196,9 +1336,6 @@ class PrivateTab(ChatTab): self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) - def get_room(self): - return self._room - def get_text_window(self): return self.text_win @@ -1207,9 +1344,9 @@ class PrivateTab(ChatTab): The user changed her nick in the corresponding muc: update the tab’s name and display a message. """ - self.get_room().add_message(_('"[%(old_nick)s]" is now known as "[%(new_nick)s]"') % {'old_nick':old_nick.replace('"', '\\"'), 'new_nick':new_nick.replace('"', '\\"')}) - new_jid = JID(self.get_room().name).bare+'/'+new_nick - self.get_room().name = new_jid + self.add_message('\x193}%(old)s\x195} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick}) + new_jid = JID(self.name).bare+'/'+new_nick + self.name = new_jid def user_left(self, status_message, from_nick): """ @@ -1217,9 +1354,9 @@ class PrivateTab(ChatTab): """ self.deactivate() if not status_message: - self.get_room().add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT.replace('"', '\\"')}) + self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT.replace('"', '\\"')}) else: - self.get_room().add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) + self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x195} has left the room (%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':get_theme().CHAR_QUIT, 'status': status_message.replace('"', '\\"')}) if self.core.current_tab() is self: self.refresh() self.core.doupdate() @@ -1229,18 +1366,26 @@ class PrivateTab(ChatTab): The user (or at least someone with the same nick) came back in the MUC """ self.activate() - self.get_room().add_message('\x194}%(spec)s \x193}%(nick)s\x195} joined the room' % {'nick':nick, 'spec':get_theme().CHAR_JOIN}) + tab = self.core.get_tab_by_name(JID(self.name).bare, MucTab) + color = None + if tab: + user = tab.get_user_by_name(nick) + if user: + color = user.color + self.add_message('\x194}%(spec)s \x19%(color)d}%(nick)s\x195} joined the room' % {'nick':nick, 'color': color or 3, 'spec':get_theme().CHAR_JOIN}) if self.core.current_tab() is self: self.refresh() self.core.doupdate() - def activate(self): self.on = True def deactivate(self): self.on = False + 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 RosterInfoTab(Tab): """ A tab, splitted in two, containing the roster and infos @@ -1255,7 +1400,7 @@ class RosterInfoTab(Tab): self.contact_info_win = windows.ContactInfoWin() self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show") self.input = self.default_help_message - self.set_color_state(get_theme().COLOR_TAB_NORMAL) + self._state = 'normal' self.key_func['^I'] = self.completion self.key_func[' '] = self.on_space self.key_func["/"] = self.on_slash @@ -1562,19 +1707,13 @@ class RosterInfoTab(Tab): self.v_separator.refresh() self.roster_win.refresh(roster) self.contact_info_win.refresh(self.roster_win.get_selected_row()) - self.information_win.refresh(self.core.informations) + self.information_win.refresh() self.tab_win.refresh() self.input.refresh() def get_name(self): return self.name - def get_color_state(self): - return self._color_state - - def set_color_state(self, color): - self._color_state = color - def on_input(self, key): if key == '^M': selected_row = self.roster_win.get_selected_row() @@ -1622,19 +1761,18 @@ class RosterInfoTab(Tab): return self.reset_help_message() def on_lose_focus(self): - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' def on_gain_focus(self): - self._color_state = get_theme().COLOR_TAB_CURRENT + self._state = 'current' if isinstance(self.input, windows.HelpText): curses.curs_set(0) else: curses.curs_set(1) - def add_message(self): - return False - def move_cursor_down(self): + if isinstance(self.input, windows.CommandInput): + return self.roster_win.move_cursor_down() self.roster_win.refresh(roster) self.contact_info_win.refresh(self.roster_win.get_selected_row()) @@ -1642,6 +1780,8 @@ class RosterInfoTab(Tab): self.core.doupdate() def move_cursor_up(self): + if isinstance(self.input, windows.CommandInput): + return self.roster_win.move_cursor_up() self.roster_win.refresh(roster) self.contact_info_win.refresh(self.roster_win.get_selected_row()) @@ -1726,12 +1866,11 @@ class ConversationTab(ChatTab): """ message_type = 'chat' def __init__(self, jid): - txt_buff = text_buffer.TextBuffer() - ChatTab.__init__(self, txt_buff) - self.color_state = get_theme().COLOR_TAB_NORMAL + ChatTab.__init__(self) + self._state = 'normal' self._name = jid # a conversation tab is linked to one specific full jid OR bare jid self.text_win = windows.TextWin() - txt_buff.add_window(self.text_win) + self._text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() self.info_header = windows.ConversationInfoWin() self.input = windows.MessageInput() @@ -1739,7 +1878,7 @@ class ConversationTab(ChatTab): self.key_func['^I'] = self.completion # commands self.commands['unquery'] = (self.command_unquery, _("Usage: /unquery\nUnquery: close the tab"), None) - self.commands['part'] = (self.command_unquery, _("Usage: /part\Part: close the tab"), None) + self.commands['close'] = (self.command_unquery, _("Usage: /close\Close: close the tab"), None) self.resize() def completion(self): @@ -1757,10 +1896,10 @@ class ConversationTab(ChatTab): needed = 'inactive' if self.core.status.show in ('xa', 'away') else 'active' msg['chat_state'] = needed msg.send() - self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick) + self.core.add_message_to_text_buffer(self._text_buffer, line, None, self.core.own_nick) logger.log_message(JID(self.get_name()).bare, self.core.own_nick, line) self.cancel_paused_delay() - self.text_win.refresh(self._room) + self.text_win.refresh() self.input.refresh() def command_unquery(self, arg): @@ -1771,7 +1910,7 @@ class ConversationTab(ChatTab): return self.need_resize = False self.text_win.resize(self.height-4-self.core.information_win_size, self.width, 1, 0) - self.text_win.rebuild_everything(self._room) + self.text_win.rebuild_everything(self._text_buffer) self.upper_bar.resize(1, self.width, 0, 0) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) self.input.resize(1, self.width, self.height-1, 0) @@ -1780,26 +1919,17 @@ class ConversationTab(ChatTab): if self.need_resize: self.resize() log.debug(' TAB Refresh: %s'%self.__class__.__name__) - self.text_win.refresh(self._room) + self.text_win.refresh() self.upper_bar.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name())) - self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self._room, self.text_win, self.chatstate) - self.info_win.refresh(self.core.informations) + self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self.text_win, self.chatstate) + self.info_win.refresh() self.tab_win.refresh() self.input.refresh() def refresh_info_header(self): - self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self._room, self.text_win, self.chatstate) + self.info_header.refresh(self.get_name(), roster.get_contact_by_jid(self.get_name()), self.text_win, self.chatstate) self.input.refresh() - def get_color_state(self): - if self.color_state == get_theme().COLOR_TAB_NORMAL or\ - self.color_state == get_theme().COLOR_TAB_CURRENT: - return self.color_state - return get_theme().COLOR_TAB_PRIVATE - - def set_color_state(self, color): - self.color_state = color - def get_name(self): return self._name @@ -1813,14 +1943,14 @@ class ConversationTab(ChatTab): return False def on_lose_focus(self): - self.set_color_state(get_theme().COLOR_TAB_NORMAL) + self._state = 'normal' self.text_win.remove_line_separator() self.text_win.add_line_separator() if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text() or not self.input.get_text().startswith('//'): self.send_chat_state('inactive') def on_gain_focus(self): - self.set_color_state(get_theme().COLOR_TAB_CURRENT) + self._state = 'current' curses.curs_set(1) if config.get('send_chat_states', 'true') == 'true' and not self.input.get_text() or not self.input.get_text().startswith('//'): self.send_chat_state('active') @@ -1837,9 +1967,6 @@ class ConversationTab(ChatTab): self.text_win.resize(self.height-4-self.core.information_win_size, self.width, 1, 0) self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0) - def get_room(self): - return self._room - def get_text_window(self): return self.text_win @@ -1855,7 +1982,7 @@ class MucListTab(Tab): """ def __init__(self, server): Tab.__init__(self) - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' self.name = server self.upper_message = windows.Topic() self.upper_message.set_message('Chatroom list on server %s (Loading)' % self.name) @@ -1969,15 +2096,12 @@ class MucListTab(Tab): return self.key_func[key]() def on_lose_focus(self): - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' def on_gain_focus(self): - self._color_state = get_theme().COLOR_TAB_CURRENT + self._state = 'current' curses.curs_set(0) - def get_color_state(self): - return self._color_state - def on_scroll_up(self): self.listview.scroll_up() @@ -1992,7 +2116,7 @@ class SimpleTextTab(Tab): """ def __init__(self, text): Tab.__init__(self) - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' self.text_win = windows.SimpleTextWin(text) self.default_help_message = windows.HelpText("“Ctrl+q”: close") self.input = self.default_help_message @@ -2035,15 +2159,12 @@ class SimpleTextTab(Tab): self.input.refresh() def on_lose_focus(self): - self._color_state = get_theme().COLOR_TAB_NORMAL + self._state = 'normal' def on_gain_focus(self): - self._color_state = get_theme().COLOR_TAB_CURRENT + self._state = 'current' curses.curs_set(0) - def get_color_state(self): - return self._color_state - def diffmatch(search, string): """ Use difflib and a loop to check if search_pattern can diff --git a/src/text_buffer.py b/src/text_buffer.py index f39f147a..eb4b7b79 100644 --- a/src/text_buffer.py +++ b/src/text_buffer.py @@ -34,19 +34,20 @@ class TextBuffer(object): def add_window(self, win): self.windows.append(win) - def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None): + def add_message(self, txt, time=None, nickname=None, nick_color=None, history=None, user=None): time = time or datetime.now() msg = Message(txt='%s\x19o'%(txt.replace('\t', ' '),), nick_color=nick_color, time=time, str_time=time.strftime("%Y-%m-%d %H:%M:%S")\ if history else time.strftime("%H:%M:%S"),\ - nickname=nickname, user=None) + nickname=nickname, user=user) + log.debug('Coucou, le message ajouté : %s' % (msg,)) self.messages.append(msg) while len(self.messages) > self.messages_nb_limit: self.messages.pop(0) ret_val = None for window in self.windows: # make the associated windows # build the lines from the new message - nb = window.build_new_message(msg) + nb = window.build_new_message(msg, history=history) if ret_val is None: ret_val = nb if window.pos != 0: diff --git a/src/windows.py b/src/windows.py index 4f2c68c6..42c9bfa0 100644 --- a/src/windows.py +++ b/src/windows.py @@ -62,9 +62,10 @@ class Win(object): self._win = None def _resize(self, height, width, y, x): - self.height, self.width, self.x, self.y = height, width, x, y if height == 0 or width == 0: + self.height, self.width = height, width return + self.height, self.width, self.x, self.y = height, width, x, y if not self._win: self._win = curses.newwin(height, width, y, x) else: @@ -191,19 +192,19 @@ class UserList(Win): def __init__(self): Win.__init__(self) self.pos = 0 - self.color_role = {'moderator': get_theme().COLOR_USER_MODERATOR, - 'participant':get_theme().COLOR_USER_PARTICIPANT, - 'visitor':get_theme().COLOR_USER_VISITOR, - 'none':get_theme().COLOR_USER_NONE, - '':get_theme().COLOR_USER_NONE - } - self.color_show = {'xa':get_theme().COLOR_STATUS_XA, - 'none':get_theme().COLOR_STATUS_NONE, - '':get_theme().COLOR_STATUS_NONE, - 'dnd':get_theme().COLOR_STATUS_DND, - 'away':get_theme().COLOR_STATUS_AWAY, - 'chat':get_theme().COLOR_STATUS_CHAT - } + self.color_role = {'moderator': lambda: get_theme().COLOR_USER_MODERATOR, + 'participant': lambda: get_theme().COLOR_USER_PARTICIPANT, + 'visitor': lambda: get_theme().COLOR_USER_VISITOR, + 'none': lambda: get_theme().COLOR_USER_NONE, + '': lambda: get_theme().COLOR_USER_NONE + } + self.color_show = {'xa': lambda: get_theme().COLOR_STATUS_XA, + 'none': lambda: get_theme().COLOR_STATUS_NONE, + '': lambda: get_theme().COLOR_STATUS_NONE, + 'dnd': lambda: get_theme().COLOR_STATUS_DND, + 'away': lambda: get_theme().COLOR_STATUS_AWAY, + 'chat': lambda: get_theme().COLOR_STATUS_CHAT + } def scroll_up(self): self.pos += self.height-1 @@ -228,11 +229,11 @@ class UserList(Win): if not user.role in self.color_role: role_col = get_theme().COLOR_USER_NONE else: - role_col = self.color_role[user.role] + role_col = self.color_role[user.role]() if not user.show in self.color_show: show_col = get_theme().COLOR_STATUS_NONE else: - show_col = self.color_show[user.show] + show_col = self.color_show[user.show]() if user.chatstate == 'composing': char = 'X' elif user.chatstate == 'active': @@ -297,7 +298,7 @@ class GlobalInfoBar(Win): self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR)) sorted_tabs = sorted(self.core.tabs, key=comp) for tab in sorted_tabs: - color = tab.get_color_state() + color = tab.color if config.get('show_inactive_tabs', 'true') == 'false' and\ color == get_theme().COLOR_TAB_NORMAL: continue @@ -340,18 +341,18 @@ class PrivateInfoWin(InfoWin): def __init__(self): InfoWin.__init__(self) - def refresh(self, room, window, chatstate): + def refresh(self, name, window, chatstate): log.debug('Refresh: %s'%self.__class__.__name__) with g_lock: self._win.erase() - self.write_room_name(room) + self.write_room_name(name) self.print_scroll_position(window) self.write_chatstate(chatstate) self.finish_line(get_theme().COLOR_INFORMATION_BAR) self._refresh() - def write_room_name(self, room): - jid = JID(room.name) + def write_room_name(self, name): + jid = JID(name) room_name, nick = jid.bare, jid.resource self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME)) txt = ' from room %s' % room_name @@ -379,7 +380,7 @@ class ConversationInfoWin(InfoWin): def __init__(self): InfoWin.__init__(self) - def refresh(self, jid, contact, text_buffer, window, chatstate): + def refresh(self, jid, contact, window, chatstate): # contact can be None, if we receive a message # from someone not in our roster. In this case, we display # only the maximum information from the message we can get. @@ -605,7 +606,7 @@ class TextWin(Win): self.built_lines.pop(0) return len(lines) - def refresh(self, room): + def refresh(self): log.debug('Refresh: %s'%self.__class__.__name__) if self.height <= 0: return |