From d837ce23811d8d201ced2bd8ee1554a21b836c2f Mon Sep 17 00:00:00 2001 From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13" Date: Tue, 7 Dec 2010 16:20:30 +0000 Subject: /list command, can join the room with J (cannot sort, search or filter yet, and lacks some information) --- src/core.py | 126 +++---- src/tab.py | 962 --------------------------------------------------- src/tabs.py | 1058 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/windows.py | 135 +++++++- 4 files changed, 1245 insertions(+), 1036 deletions(-) delete mode 100644 src/tab.py create mode 100644 src/tabs.py diff --git a/src/core.py b/src/core.py index 8a5b4753..5e7ddaea 100644 --- a/src/core.py +++ b/src/core.py @@ -39,9 +39,10 @@ from sleekxmpp.xmlstream.stanzabase import JID log = logging.getLogger(__name__) import multiuserchat as muc +import tabs + from connection import connection from config import config -from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab from logger import logger from user import User from room import Room @@ -83,8 +84,8 @@ class Core(object): self.stdscr = curses.initscr() self.init_curses(self.stdscr) self.xmpp = xmpp - default_tab = InfoTab(self, "Info") if self.xmpp.anon\ - else RosterInfoTab(self) + default_tab = tabs.InfoTab(self, "Info") if self.xmpp.anon\ + else tabs.RosterInfoTab(self) default_tab.on_gain_focus() self.tabs = [default_tab] # a unique buffer used to store global informations @@ -183,7 +184,7 @@ class Core(object): assert resource self.information('%s is offline' % (resource.get_jid()), "Roster") contact.remove_resource(resource) - if isinstance(self.current_tab(), RosterInfoTab): + if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() def on_got_online(self, presence): @@ -315,7 +316,7 @@ class Core(object): room.own_nick = new_nick # also change our nick in all private discussion of this room for _tab in self.tabs: - if isinstance(_tab, PrivateTab) and _tab.get_name().split('/', 1)[0] == room.name: + if isinstance(_tab, tabs.PrivateTab) and _tab.get_name().split('/', 1)[0] == room.name: _tab.get_room().own_nick = new_nick user.change_nick(new_nick) self.add_message_to_text_buffer(room, _('"[%(old)s]" is now known as "[%(new)s]"') % {'old':from_nick.replace('"', '\\"'), 'new':new_nick.replace('"', '\\"')}, colorized=True) @@ -497,7 +498,7 @@ class Core(object): resource.set_presence(status) resource.set_priority(priority) resource.set_status(status_message) - if isinstance(self.current_tab(), RosterInfoTab): + if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() def on_roster_update(self, iq): @@ -523,7 +524,7 @@ class Core(object): contact.set_subscription(item.attrib['subscription']) groups = item.findall('{jabber:iq:roster}group') roster.edit_groups_of_contact(contact, [group.text for group in groups]) - if isinstance(self.current_tab(), RosterInfoTab): + if isinstance(self.current_tab(), tabs.RosterInfoTab): self.refresh_window() def call_for_resize(self): @@ -578,7 +579,7 @@ class Core(object): Return the room of the ConversationTab with the given jid """ for tab in self.tabs: - if isinstance(tab, ConversationTab): + if isinstance(tab, tabs.ConversationTab): if tab.get_name() == jid: return tab.get_room() return None @@ -597,8 +598,8 @@ class Core(object): returns the room that has this name """ for tab in self.tabs: - if (isinstance(tab, MucTab) or - isinstance(tab, PrivateTab)) and tab.get_name() == name: + if (isinstance(tab, tabs.MucTab) or + isinstance(tab, tabs.PrivateTab)) and tab.get_name() == name: return tab.get_room() return None @@ -629,12 +630,11 @@ class Core(object): self.current_tab().refresh(self.tabs, self.information_buffer, roster) self.doupdate() - def open_new_room(self, room, nick, focus=True): + def add_tab(self, new_tab, focus=False): """ - Open a new MucTab containing a muc Room, using the specified nick + Appends the new_tab in the tab list and + focus it if focus==True """ - r = Room(room, nick) - new_tab = MucTab(self, r) if self.current_tab().nb == 0: self.tabs.append(new_tab) else: @@ -644,6 +644,14 @@ class Core(object): break if focus: self.command_win("%s" % new_tab.nb) + + def open_new_room(self, room, nick, focus=True): + """ + Open a new tab.MucTab containing a muc Room, using the specified nick + """ + r = Room(room, nick) + new_tab = tabs.MucTab(self, r) + self.add_tab(new_tab, focus) self.refresh_window() def go_to_roster(self): @@ -671,6 +679,10 @@ class Core(object): if tab.get_color_state() == theme.COLOR_TAB_NEW_MESSAGE: self.command_win('%s' % tab.nb) return + for tab in self.tabs: + if isinstance(tab, tabs.ChatTab) and not tab.input.is_empty(): + self.command_win('%s' % tab.nb) + return def rotate_rooms_right(self, args=None): """ @@ -733,24 +745,16 @@ class Core(object): open a new conversation tab and focus it if needed """ text_buffer = TextBuffer() - new_tab = ConversationTab(self, text_buffer, jid) + new_tab = tabs.ConversationTab(self, text_buffer, jid) # insert it in the rooms - if self.current_tab().nb == 0: - self.tabs.append(new_tab) - else: - for ta in self.tabs: - if ta.nb == 0: - self.tabs.insert(self.tabs.index(ta), new_tab) - break - if focus: # focus the room if needed - self.command_win('%s' % (new_tab.nb)) + self.add_tab(new_tab, focus) self.refresh_window() return new_tab def open_private_window(self, room_name, user_nick, focus=True): complete_jid = room_name+'/'+user_nick for tab in self.tabs: # if the room exists, focus it and return - if isinstance(tab, PrivateTab): + if isinstance(tab, tabs.PrivateTab): if tab.get_name() == complete_jid: self.command_win('%s' % tab.nb) return @@ -760,17 +764,9 @@ class Core(object): return None own_nick = room.own_nick r = Room(complete_jid, own_nick) # PrivateRoom here - new_tab = PrivateTab(self, r) + new_tab = tabs.PrivateTab(self, r) # insert it in the tabs - if self.current_tab().nb == 0: - self.tabs.append(new_tab) - else: - for ta in self.tabs: - if ta.nb == 0: - self.tabs.insert(self.tabs.index(ta), new_tab) - break - if focus: # focus the room if needed - self.command_win('%s' % (new_tab.nb)) + self.add_tab(new_tab, focus) # self.window.new_room(r) self.refresh_window() return r @@ -860,6 +856,29 @@ class Core(object): msg = _('Unknown command: %s') % args[0] self.information(msg) + def command_list(self, arg): + """ + /list + Opens a MucListTab containing the list of the room in the specified server + """ + args = arg.split() + if len(args) > 1: + self.command_help('list') + return + elif len(args) == 0: + if not isinstance(self.current_tab(), tabs.MucTab): + return self.information('Warning: Please provide a server') + server = JID(self.current_tab().get_name()).server + else: + server = arg.strip() + list_tab = tabs.MucListTab(self, server) + self.add_tab(list_tab, True) + res = self.xmpp.plugin['xep_0030'].getItems(server) + items = [{'node-part':JID(item[0]).user, + 'jid': item[0], + 'name': item[2]} for item in res['disco_items'].getItems()] + list_tab.listview.add_lines(items) + def command_whois(self, arg): """ /whois @@ -948,29 +967,18 @@ class Core(object): serv = jid.server serv_list = [] for tab in self.tabs: - if isinstance(tab, MucTab): + if isinstance(tab, tabs.MucTab): serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host)) the_input.auto_completion(serv_list, '') return True - def command_list(self, arg): - """ - Opens a MucListTab for the specified server - """ - args = arg.split() - if len(args) != 1: - self.command_win('list') - return - server = args[1] - # TODO - def completion_list(self, the_input): """ """ txt = the_input.get_text() muc_serv_list = [] for tab in self.tabs: # TODO, also from an history - if isinstance(tab, MucTab) and\ + if isinstance(tab, tabs.MucTab) and\ tab.get_name() not in muc_serv_list: muc_serv_list.append(tab.get_name()) if muc_serv_list: @@ -984,7 +992,7 @@ class Core(object): password = None if len(args) == 0: t = self.current_tab() - if not isinstance(t, MucTab) and not isinstance(t, PrivateTab): + if not isinstance(t, tabs.MucTab) and not isinstance(t, tabs.PrivateTab): return room = t.get_name() nick = t.get_room().own_nick @@ -999,7 +1007,7 @@ class Core(object): nick = info[1] if info[0] == '': # happens with /join /nickname, which is OK t = self.current_tab() - if not isinstance(t, MucTab): + if not isinstance(t, tabs.MucTab): return room = t.get_name() if nick == '': @@ -1009,7 +1017,7 @@ class Core(object): if not is_jid(room): # no server is provided, like "/join hello" # use the server of the current room if available # check if the current room's name has a server - if isinstance(self.current_tab(), MucTab) and\ + if isinstance(self.current_tab(), tabs.MucTab) and\ is_jid(self.current_tab().get_name()): room += '@%s' % jid_get_domain(self.current_tab().get_name()) else: # no server could be found, print a message and return @@ -1037,7 +1045,7 @@ class Core(object): """ args = arg.split() nick = None - if not isinstance(self.current_tab(), MucTab): + if not isinstance(self.current_tab(), tabs.MucTab): return if len(args) == 0: room = self.current_tab().get_room() @@ -1115,7 +1123,7 @@ class Core(object): else: msg = None for tab in self.tabs: - if isinstance(tab, MucTab) and tab.get_room().joined: + 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) def command_away(self, arg): @@ -1141,8 +1149,8 @@ class Core(object): Close the given tab. If None, close the current one """ tab = tab or self.current_tab() - if isinstance(tab, RosterInfoTab) or\ - isinstance(tab, InfoTab): + if isinstance(tab, tabs.RosterInfoTab) or\ + isinstance(tab, tabs.InfoTab): return # The tab 0 should NEVER be closed tab.on_close() self.tabs.remove(tab) @@ -1155,8 +1163,8 @@ class Core(object): Opens the link in a browser, or join the room, or add the JID, or copy it in the clipboard """ - if not isinstance(self.current_tab(), MucTab) and\ - not isinstance(self.current_tab(), PrivateTab): + if not isinstance(self.current_tab(), tabs.MucTab) and\ + not isinstance(self.current_tab(), tabs.PrivateTab): return args = arg.split() if len(args) > 2: @@ -1229,7 +1237,7 @@ class Core(object): else: msg = None for tab in self.tabs: - if isinstance(tab, MucTab): + if isinstance(tab, tabs.MucTab): muc.leave_groupchat(self.xmpp, tab.get_room().name, tab.get_room().own_nick, msg) self.xmpp.disconnect() self.running = False @@ -1283,5 +1291,5 @@ class Core(object): self.current_tab().just_before_refresh() curses.doupdate() -# # global core object +# global core object core = Core(connection) diff --git a/src/tab.py b/src/tab.py deleted file mode 100644 index 4a76fca4..00000000 --- a/src/tab.py +++ /dev/null @@ -1,962 +0,0 @@ -# Copyright 2010 Le Coz Florent -# -# This file is part of Poezio. -# -# Poezio is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# Poezio is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Poezio. If not, see . - -""" -a Tab object is a way to organize various Windows (see windows.py) -around the screen at once. -A tab is then composed of multiple Buffer. -Each Tab object has different refresh() and resize() methods, defining how its -Buffer are displayed, resized, etc -""" - -MIN_WIDTH = 50 -MIN_HEIGHT = 16 - -from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset, - gettext as _) - -import logging -log = logging.getLogger(__name__) - -import windows -import theme -import curses -import difflib -import shlex - -from sleekxmpp.xmlstream.stanzabase import JID -from config import config -from roster import RosterGroup, roster -from contact import Contact, Resource -import multiuserchat as muc - -class Tab(object): - number = 0 - - def __init__(self, core): - self.core = core # a pointer to core, to access its attributes (ugly?) - self.nb = Tab.number - Tab.number += 1 - self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() - if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: - self.visible = False - else: - self.visible = True - self.key_func = {} # each tab should add their keys in there - # and use them in on_input - self.commands = {} # and their own commands - - def complete_commands(self, the_input): - """ - Does command completion on the specified input for both global and tab-specific - commands. - This should be called from the completion method (on tab, for example), passing - the input where completion is to be made. - It can completion the command name itself or an argument of the command. - Returns True if a completion was made, False else. - """ - txt = the_input.get_text() - # check if this is a command - if txt.startswith('/') and not txt.startswith('//'): - # check if we are in the middle of the command name - if len(txt.split()) > 1 or\ - (txt.endswith(' ') and not the_input.last_completion): - command_name = txt.split()[0][1:] - if command_name in self.core.commands: - command = self.core.commands[command_name] - elif command_name in self.commands: - command = self.commands[command_name] - else: # Unknown command, cannot complete - return False - if command[2] is None: - return False # There's no completion functio - else: - return command[2](the_input) - else: - # complete the command's name - words = ['/%s'%(name) for name in list(self.core.commands.keys())] +\ - ['/%s'% (name) for name in list(self.commands.keys())] - the_input.auto_completion(words, '') - return True - return False - - def resize(self): - self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() - if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: - self.visible = False - else: - self.visible = True - - def refresh(self, tabs, informations, roster): - """ - Called on each screen refresh (when something has changed) - """ - raise NotImplementedError - - def get_color_state(self): - """ - returns the color that should be used in the GlobalInfoBar - """ - raise NotImplementedError - - def set_color_state(self, color): - """ - set the color state - """ - raise NotImplementedError - - def get_name(self): - """ - get the name of the tab - """ - raise NotImplementedError - - def on_input(self, key): - raise NotImplementedError - - def on_lose_focus(self): - """ - called when this tab loses the focus. - """ - raise NotImplementedError - - def on_gain_focus(self): - """ - called when this tab gains the focus. - """ - raise NotImplementedError - - 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 - """ - raise NotImplementedError - - def on_scroll_down(self): - """ - Defines what happens when we scrol down - """ - raise NotImplementedError - - def on_scroll_up(self): - """ - Defines what happens when we scrol down - """ - raise NotImplementedError - - def on_info_win_size_changed(self): - """ - Called when the window with the informations is resized - """ - raise NotImplementedError - - def just_before_refresh(self): - """ - Method called just before the screen refresh. - Particularly useful to move the cursor at the - correct position. - """ - raise NotImplementedError - - def on_close(self): - """ - Called when the tab is to be closed - """ - raise NotImplementedError - -class InfoTab(Tab): - """ - The information tab, used to display global informations - when using a anonymous account - """ - def __init__(self, core, name): - Tab.__init__(self, core) - self.tab_win = windows.GlobalInfoBar() - self.text_win = windows.TextWin() - self.input = windows.Input() - self.name = name - self.color_state = theme.COLOR_TAB_NORMAL - self.resize() - - def resize(self): - Tab.resize(self) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.text_win.resize(self.height-2, self.width, 0, 0, self.core.stdscr) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - - def refresh(self, tabs, informations, _): - if not self.visible: - return - self.text_win.refresh(informations) - self.tab_win.refresh(tabs, tabs[0]) - 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): - return - - def on_input(self, key): - return self.input.do_command(key) - - def on_lose_focus(self): - self.color_state = theme.COLOR_TAB_NORMAL - - def on_gain_focus(self): - self.color_state = theme.COLOR_TAB_CURRENT - curses.curs_set(0) - - def on_scroll_up(self): - pass - - def on_scroll_down(self): - pass - - def on_info_win_size_changed(self): - return - - def just_before_refresh(self): - return - - def on_close(self): - return - -class ChatTab(Tab): - """ - A tab containing a chat of any type. - Just use this class instead of Tab if the tab needs a recent-words completion - Also, \n, ^J and ^M are already bound to on_enter - And also, add the /say command - """ - def __init__(self, core, room): - Tab.__init__(self, core) - self._room = room - self.key_func['M-/'] = self.last_words_completion - self.key_func['^J'] = self.on_enter - self.key_func['^M'] = self.on_enter - self.key_func['\n'] = self.on_enter - self.commands['say'] = (self.command_say, - _("""Usage: /say \nSay: Just send the message. - Useful if you want your message to begin with a '/'"""), None) - - def last_words_completion(self): - """ - Complete the input with words recently said - """ - # build the list of the recent words - char_we_dont_want = [',', '(', ')', '.', '"', '\'', ' ', # The last one is nbsp - '’', '“', '”', ':', ';', '[', ']', '{', '}'] - words = list() - for msg in self._room.messages[:-40:-1]: - if not msg: - continue - txt = msg.txt - for char in char_we_dont_want: - txt = txt.replace(char, ' ') - for word in txt.split(): - if len(word) >= 4 and word not in words: - words.append(word) - self.input.auto_completion(words, ' ') - - def on_enter(self): - txt = self.input.key_enter() - if txt.startswith('/') and not txt.startswith('//') and\ - not txt.startswith('/me '): - command = txt.strip().split()[0][1:] - arg = txt[2+len(command):] # jump the '/' and the ' ' - if command in self.core.commands: # check global commands - self.core.commands[command][0](arg) - elif command in self.commands: # check tab-specific commands - self.commands[command][0](arg) - else: - self.core.information(_("Unknown command (%s)") % (command), _('Error')) - else: - if txt.startswith('//'): - txt = txt[1:] - self.command_say(txt) - - def command_say(self, line): - raise NotImplementedError - -class MucTab(ChatTab): - """ - The tab containing a multi-user-chat room. - It contains an userlist, an input, a topic, an information and a chat zone - """ - def __init__(self, core, room): - ChatTab.__init__(self, core, room) - self.topic_win = windows.Topic() - self.text_win = windows.TextWin() - self.v_separator = windows.VerticalSeparator() - self.user_win = windows.UserList() - self.info_header = windows.MucInfoWin() - self.info_win = windows.TextWin() - self.tab_win = windows.GlobalInfoBar() - self.input = windows.MessageInput() - self.ignores = [] # set of Users - # keys - self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion - # commands - self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore \nIgnore: Ignore a specified nickname."), None) - self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore \nUnignore: Remove the specified nickname from the ignore list."), None) - self.commands['kick'] = (self.command_kick, _("Usage: /kick [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason."), None) - self.commands['topic'] = (self.command_topic, _("Usage: /topic \nTopic: Change the subject of the room"), None) - self.commands['query'] = (self.command_query, _('Usage: /query [message]\nQuery: Open a private conversation with . 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]\n Part: disconnect from a room. You can specify an optional message."), None) - self.commands['nick'] = (self.command_nick, _("Usage: /nick \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.resize() - - def command_recolor(self, arg): - """ - Re-assign color to the participants of the room - """ - room = self.get_room() - i = 0 - compare_users = lambda x: x.last_talked - users = list(room.users) - # search our own user, to remove it from the room - for user in users: - if user.nick == room.own_nick: - users.remove(user) - nb_color = len(theme.LIST_COLOR_NICKNAMES) - for user in sorted(users, key=compare_users, reverse=True): - user.color = theme.LIST_COLOR_NICKNAMES[i % nb_color] - i+= 1 - self.core.refresh_window() - - def command_nick(self, arg): - """ - /nick - """ - try: - args = shlex.split(arg) - except ValueError as error: - return self.core.information(str(error), _("Error")) - if len(args) != 1: - return - nick = args[0] - room = self.get_room() - if not room.joined: - return - muc.change_nick(self.core.xmpp, room.name, nick) - - def command_part(self, arg): - """ - /part [msg] - """ - args = arg.split() - reason = None - 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) - self.core.close_tab() - - def command_query(self, arg): - """ - /query [message] - """ - try: - args = shlex.split(arg) - except ValueError as error: - return self.core.information(str(error), _("Error")) - if len(args) < 1: - return - nick = args[0] - room = self.get_room() - r = None - for user in room.users: - if user.nick == nick: - r = self.core.open_private_window(room.name, user.nick) - if r and len(args) > 1: - msg = arg[len(nick)+1:] - muc.send_private_message(self.core.xmpp, r.name, msg) - self.core.add_message_to_text_buffer(r, msg, None, r.own_nick) - - def command_topic(self, arg): - """ - /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) - return - subject = arg - muc.change_subject(self.core.xmpp, self.get_room().name, subject) - - def command_kick(self, arg): - """ - /kick [reason] - """ - try: - args = shlex.split(arg) - except ValueError as error: - return self.core.information(str(error), _("Error")) - if len(args) < 1: - self.core.command_help('kick') - return - nick = args[0] - if len(args) >= 2: - reason = ' '.join(args[1:]) - else: - reason = '' - if not self.get_room().joined: - return - res = muc.eject_user(self.core.xmpp, self.get_name(), nick, reason) - if res['type'] == 'error': - self.core.room_error(res, self.get_name()) - - def command_say(self, line): - muc.send_groupchat_message(self.core.xmpp, self.get_name(), line) - - def command_ignore(self, arg): - """ - /ignore - """ - try: - args = shlex.split(arg) - except ValueError as error: - return self.core.information(str(error), _("Error")) - if len(args) != 1: - self.core.command_help('ignore') - return - nick = args[0] - user = self._room.get_user_by_name(nick) - if not user: - self.core.information(_('%s is not in the room') % nick) - elif user in self.ignores: - self.core.information(_('%s is already ignored') % nick) - else: - self.ignores.append(user) - self.core.information(_("%s is now ignored") % nick, 'info') - - def command_unignore(self, arg): - """ - /unignore - """ - try: - args = shlex.split(arg) - except ValueError as error: - return self.core.information(str(error), _("Error")) - if len(args) != 1: - self.core.command_help('unignore') - return - nick = args[0] - user = self._room.get_user_by_name(nick) - if not user: - self.core.information(_('%s is not in the room') % nick) - elif user not in self.ignores: - self.core.information(_('%s is not ignored') % nick) - else: - self.ignores.remove(user) - self.core.information(_('%s is now unignored') % nick) - - def resize(self): - """ - Resize the whole window. i.e. all its sub-windows - """ - Tab.resize(self) - text_width = (self.width//10)*9 - self.topic_win.resize(1, self.width, 0, 0, self.core.stdscr) - self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) - self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10), self.core.stdscr) - self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, self.core.stdscr) - self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - - def refresh(self, tabs, informations, _): - if not self.visible: - return - self.topic_win.refresh(self._room.topic) - self.text_win.refresh(self._room) - self.v_separator.refresh() - self.user_win.refresh(self._room.users) - self.info_header.refresh(self._room) - self.info_win.refresh(informations) - self.tab_win.refresh(tabs, tabs[0]) - self.input.refresh() - - def on_input(self, key): - if key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key) - return False - - def completion(self): - """ - Called when Tab is pressed, complete the nickname in the input - """ - if self.complete_commands(self.input): - return - compare_users = lambda x: x.last_talked - word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)] - after = config.get('after_completion', ',')+" " - if ' ' not in self.input.get_text() or (self.input.last_completion and\ - self.input.get_text()[:-len(after)] == self.input.last_completion): - add_after = after - else: - add_after = ' ' - self.input.auto_completion(word_list, add_after) - - def get_color_state(self): - return self._room.color_state - - def set_color_state(self, color): - self._room.set_color_state(color) - - def get_name(self): - return self._room.name - - def get_room(self): - return self._room - - def on_lose_focus(self): - self._room.set_color_state(theme.COLOR_TAB_NORMAL) - self._room.remove_line_separator() - self._room.add_line_separator() - - def on_gain_focus(self): - self._room.set_color_state(theme.COLOR_TAB_CURRENT) - curses.curs_set(1) - - def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) - - def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) - - def on_info_win_size_changed(self): - text_width = (self.width//10)*9 - self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) - self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - - def just_before_refresh(self): - return - - def on_close(self): - return - -class PrivateTab(ChatTab): - """ - The tab containg a private conversation (someone from a MUC) - """ - def __init__(self, core, room): - ChatTab.__init__(self, core, room) - self.text_win = windows.TextWin() - self.info_header = windows.PrivateInfoWin() - self.info_win = windows.TextWin() - self.tab_win = windows.GlobalInfoBar() - self.input = windows.MessageInput() - # keys - self.key_func['^I'] = self.completion - self.key_func['M-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.resize() - - def completion(self): - self.complete_commands(self.input) - - def command_say(self, line): - muc.send_private_message(self.core.xmpp, self.get_name(), line) - self.core.add_message_to_text_buffer(self.get_room(), line, None, self.get_room().own_nick) - - def command_unquery(self, arg): - """ - /unquery - """ - self.core.close_tab() - - def resize(self): - Tab.resize(self) - self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) - self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - - def refresh(self, tabs, informations, _): - if not self.visible: - return - self.text_win.refresh(self._room) - self.info_header.refresh(self._room) - self.info_win.refresh(informations) - self.tab_win.refresh(tabs, tabs[0]) - self.input.refresh() - - def get_color_state(self): - if self._room.color_state == theme.COLOR_TAB_NORMAL or\ - self._room.color_state == theme.COLOR_TAB_CURRENT: - return self._room.color_state - return theme.COLOR_TAB_PRIVATE - - def set_color_state(self, color): - self._room.color_state = color - - def get_name(self): - return self._room.name - - def on_input(self, key): - if key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key) - return False - - def on_lose_focus(self): - self._room.set_color_state(theme.COLOR_TAB_NORMAL) - self._room.remove_line_separator() - self._room.add_line_separator() - - def on_gain_focus(self): - self._room.set_color_state(theme.COLOR_TAB_CURRENT) - curses.curs_set(1) - - def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) - - def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) - - def on_info_win_size_changed(self): - self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) - self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - - def get_room(self): - return self._room - - def just_before_refresh(self): - return - - def on_close(self): - return - -class RosterInfoTab(Tab): - """ - A tab, splitted in two, containing the roster and infos - """ - def __init__(self, core): - Tab.__init__(self, core) - self.name = "Roster" - self.v_separator = windows.VerticalSeparator() - self.tab_win = windows.GlobalInfoBar() - self.info_win = windows.TextWin() - self.roster_win = windows.RosterWin() - 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(theme.COLOR_TAB_NORMAL) - self.key_func['^I'] = self.completion - self.key_func['M-i'] = self.completion - self.key_func["^J"] = self.on_enter - self.key_func["^M"] = self.on_enter - self.key_func[' '] = self.on_space - self.key_func["/"] = self.on_slash - self.key_func["KEY_UP"] = self.move_cursor_up - self.key_func["KEY_DOWN"] = self.move_cursor_down - self.key_func["o"] = self.toggle_offline_show - self.key_func["^F"] = self.start_search - self.resize() - - def resize(self): - Tab.resize(self) - roster_width = self.width//2 - info_width = self.width-roster_width-1 - self.v_separator.resize(self.height-2, 1, 0, roster_width, self.core.stdscr) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.info_win.resize(self.height-2, info_width, 0, roster_width+1, self.core.stdscr) - self.roster_win.resize(self.height-2-3, roster_width, 0, 0, self.core.stdscr) - self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, self.core.stdscr) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - - def completion(self): - # Check if we are entering a command (with the '/' key) - if isinstance(self.input, windows.CommandInput) and\ - not self.input.help_message: - self.complete_commands(self.input) - - def refresh(self, tabs, informations, roster): - if not self.visible: - return - self.v_separator.refresh() - self.roster_win.refresh(roster) - self.contact_info_win.refresh(self.roster_win.get_selected_row()) - self.info_win.refresh(informations) - self.tab_win.refresh(tabs, tabs[0]) - 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): - res = self.input.do_command(key) - if res: - return True - if key in self.key_func: - return self.key_func[key]() - - def toggle_offline_show(self): - """ - Show or hide offline contacts - """ - option = 'roster_show_offline' - if config.get(option, 'false') == 'false': - config.set_and_save(option, 'true') - else: - config.set_and_save(option, 'false') - return True - - def on_slash(self): - """ - '/' is pressed, we enter "input mode" - """ - curses.curs_set(1) - self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - self.input.do_command("/") # we add the slash - - def reset_help_message(self, _=None): - curses.curs_set(0) - self.input = self.default_help_message - return True - - def execute_slash_command(self, txt): - if txt.startswith('/'): - self.core.execute(txt) - return self.reset_help_message() - - def on_lose_focus(self): - self._color_state = theme.COLOR_TAB_NORMAL - - def on_gain_focus(self): - self._color_state = theme.COLOR_TAB_CURRENT - curses.curs_set(0) - - def add_message(self): - return False - - def move_cursor_down(self): - self.roster_win.move_cursor_down() - return True - - def move_cursor_up(self): - self.roster_win.move_cursor_up() - return True - - def on_scroll_down(self): - # Scroll info win - pass - - def on_scroll_up(self): - # Scroll info down - pass - - def on_info_win_size_changed(self): - pass - - def on_space(self): - selected_row = self.roster_win.get_selected_row() - if isinstance(selected_row, RosterGroup) or\ - isinstance(selected_row, Contact): - selected_row.toggle_folded() - return True - - def on_enter(self): - selected_row = self.roster_win.get_selected_row() - self.core.on_roster_enter_key(selected_row) - return selected_row - - def start_search(self): - """ - Start the search. The input should appear with a short instruction - in it. - """ - curses.curs_set(1) - self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - return True - - def set_roster_filter(self, txt): - roster._contact_filter = (jid_and_name_match, txt) - self.roster_win.refresh(roster) - return False - - def on_search_terminate(self, txt): - curses.curs_set(0) - roster._contact_filter = None - self.reset_help_message() - return True - - def just_before_refresh(self): - return - - def on_close(self): - return - -class ConversationTab(ChatTab): - """ - The tab containg a normal conversation (someone from our roster) - """ - def __init__(self, core, text_buffer, jid): - ChatTab.__init__(self, core, text_buffer) - self.color_state = theme.COLOR_TAB_NORMAL - self._name = jid # a conversation tab is linked to one specific full jid OR bare jid - self.text_win = windows.TextWin() - self.upper_bar = windows.ConversationStatusMessageWin() - self.info_header = windows.ConversationInfoWin() - self.info_win = windows.TextWin() - self.tab_win = windows.GlobalInfoBar() - self.input = windows.MessageInput() - # keys - self.key_func['^I'] = self.completion - self.key_func['M-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.resize() - - def completion(self): - self.complete_commands(self.input) - - def command_say(self, line): - muc.send_private_message(self.core.xmpp, self.get_name(), line) - self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick) - - def command_unquery(self, arg): - """ - /unquery - """ - self.core.close_tab() - - def resize(self): - Tab.resize(self) - self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 1, 0, self.core.stdscr) - self.upper_bar.resize(1, self.width, 0, 0, self.core.stdscr) - self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) - self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) - - def refresh(self, tabs, informations, roster): - if not self.visible: - return - self.text_win.refresh(self._room) - 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.info_win.refresh(informations) - self.tab_win.refresh(tabs, tabs[0]) - self.input.refresh() - - def get_color_state(self): - if self.color_state == theme.COLOR_TAB_NORMAL or\ - self.color_state == theme.COLOR_TAB_CURRENT: - return self.color_state - return theme.COLOR_TAB_PRIVATE - - def set_color_state(self, color): - self.color_state = color - - def get_name(self): - return self._name - - def on_input(self, key): - if key in self.key_func: - self.key_func[key]() - return False - self.input.do_command(key) - return False - - def on_lose_focus(self): - self.set_color_state(theme.COLOR_TAB_NORMAL) - self._room.remove_line_separator() - self._room.add_line_separator() - - def on_gain_focus(self): - self.set_color_state(theme.COLOR_TAB_CURRENT) - curses.curs_set(1) - - def on_scroll_up(self): - self._room.scroll_up(self.text_win.height-1) - - def on_scroll_down(self): - self._room.scroll_down(self.text_win.height-1) - - def on_info_win_size_changed(self): - self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) - self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) - self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) - - def get_room(self): - return self._room - - def just_before_refresh(self): - return - - def on_close(self): - return - -def diffmatch(search, string): - """ - Use difflib and a loop to check if search_pattern can - be 'almost' found INSIDE a string. - 'almost' being defined by difflib - """ - l = len(search) - ratio = 0.7 - for i in range(len(string) - l + 1): - if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio: - return True - return False - -def jid_and_name_match(contact, txt): - """ - A function used to know if a contact in the roster should - be shown in the roster - """ - ratio = 0.7 - if not txt: - return True # Everything matches when search is empty - user = JID(contact.get_bare_jid()).user - if diffmatch(txt, user): - return True - if contact.get_name() and diffmatch(txt, contact.get_name()): - return True - return False diff --git a/src/tabs.py b/src/tabs.py new file mode 100644 index 00000000..2dc6a017 --- /dev/null +++ b/src/tabs.py @@ -0,0 +1,1058 @@ +# Copyright 2010 Le Coz Florent +# +# This file is part of Poezio. +# +# Poezio is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# Poezio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Poezio. If not, see . + +""" +a Tab object is a way to organize various Windows (see windows.py) +around the screen at once. +A tab is then composed of multiple Buffer. +Each Tab object has different refresh() and resize() methods, defining how its +Buffer are displayed, resized, etc +""" + +MIN_WIDTH = 50 +MIN_HEIGHT = 16 + +from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset, + gettext as _) + +import logging +log = logging.getLogger(__name__) + +import windows +import theme +import curses +import difflib +import shlex + +from sleekxmpp.xmlstream.stanzabase import JID +from config import config +from roster import RosterGroup, roster +from contact import Contact, Resource +import multiuserchat as muc + +class Tab(object): + number = 0 + + def __init__(self, core): + self.core = core # a pointer to core, to access its attributes (ugly?) + self.nb = Tab.number + Tab.number += 1 + self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() + if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: + self.visible = False + else: + self.visible = True + self.key_func = {} # each tab should add their keys in there + # and use them in on_input + self.commands = {} # and their own commands + + def complete_commands(self, the_input): + """ + Does command completion on the specified input for both global and tab-specific + commands. + This should be called from the completion method (on tab, for example), passing + the input where completion is to be made. + It can completion the command name itself or an argument of the command. + Returns True if a completion was made, False else. + """ + txt = the_input.get_text() + # check if this is a command + if txt.startswith('/') and not txt.startswith('//'): + # check if we are in the middle of the command name + if len(txt.split()) > 1 or\ + (txt.endswith(' ') and not the_input.last_completion): + command_name = txt.split()[0][1:] + if command_name in self.core.commands: + command = self.core.commands[command_name] + elif command_name in self.commands: + command = self.commands[command_name] + else: # Unknown command, cannot complete + return False + if command[2] is None: + return False # There's no completion functio + else: + return command[2](the_input) + else: + # complete the command's name + words = ['/%s'%(name) for name in list(self.core.commands.keys())] +\ + ['/%s'% (name) for name in list(self.commands.keys())] + the_input.auto_completion(words, '') + return True + return False + + def resize(self): + self.size = (self.height, self.width) = self.core.stdscr.getmaxyx() + if self.height < MIN_HEIGHT or self.width < MIN_WIDTH: + self.visible = False + else: + self.visible = True + + def refresh(self, tabs, informations, roster): + """ + Called on each screen refresh (when something has changed) + """ + raise NotImplementedError + + def get_color_state(self): + """ + returns the color that should be used in the GlobalInfoBar + """ + raise NotImplementedError + + def set_color_state(self, color): + """ + set the color state + """ + raise NotImplementedError + + def get_name(self): + """ + get the name of the tab + """ + raise NotImplementedError + + def on_input(self, key): + pass + + def on_lose_focus(self): + """ + called when this tab loses the focus. + """ + pass + + def on_gain_focus(self): + """ + called when this tab gains the focus. + """ + pass + + 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 + + def on_scroll_down(self): + """ + Defines what happens when we scrol down + """ + pass + + def on_scroll_up(self): + """ + Defines what happens when we scrol down + """ + pass + + def on_info_win_size_changed(self): + """ + Called when the window with the informations is resized + """ + pass + + def just_before_refresh(self): + """ + Method called just before the screen refresh. + Particularly useful to move the cursor at the + correct position. + """ + pass + + def on_close(self): + """ + Called when the tab is to be closed + """ + pass + +class InfoTab(Tab): + """ + The information tab, used to display global informations + when using a anonymous account + """ + def __init__(self, core, name): + Tab.__init__(self, core) + self.tab_win = windows.GlobalInfoBar() + self.text_win = windows.TextWin() + self.input = windows.Input() + self.name = name + self.color_state = theme.COLOR_TAB_NORMAL + self.resize() + + def resize(self): + Tab.resize(self) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.text_win.resize(self.height-2, self.width, 0, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.text_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + 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): + return + + def on_input(self, key): + return self.input.do_command(key) + + def on_lose_focus(self): + self.color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self.color_state = theme.COLOR_TAB_CURRENT + curses.curs_set(0) + + def on_scroll_up(self): + pass + + def on_scroll_down(self): + pass + + def on_info_win_size_changed(self): + return + + def just_before_refresh(self): + return + + def on_close(self): + return + +class ChatTab(Tab): + """ + A tab containing a chat of any type. + Just use this class instead of Tab if the tab needs a recent-words completion + Also, \n, ^J and ^M are already bound to on_enter + And also, add the /say command + """ + def __init__(self, core, room): + Tab.__init__(self, core) + self._room = room + self.key_func['M-/'] = self.last_words_completion + self.key_func['^J'] = self.on_enter + self.key_func['^M'] = self.on_enter + self.key_func['\n'] = self.on_enter + self.commands['say'] = (self.command_say, + _("""Usage: /say \nSay: Just send the message. + Useful if you want your message to begin with a '/'"""), None) + + def last_words_completion(self): + """ + Complete the input with words recently said + """ + # build the list of the recent words + char_we_dont_want = [',', '(', ')', '.', '"', '\'', ' ', # The last one is nbsp + '’', '“', '”', ':', ';', '[', ']', '{', '}'] + words = list() + for msg in self._room.messages[:-40:-1]: + if not msg: + continue + txt = msg.txt + for char in char_we_dont_want: + txt = txt.replace(char, ' ') + for word in txt.split(): + if len(word) >= 4 and word not in words: + words.append(word) + self.input.auto_completion(words, ' ') + + def on_enter(self): + txt = self.input.key_enter() + if txt.startswith('/') and not txt.startswith('//') and\ + not txt.startswith('/me '): + command = txt.strip().split()[0][1:] + arg = txt[2+len(command):] # jump the '/' and the ' ' + if command in self.core.commands: # check global commands + self.core.commands[command][0](arg) + elif command in self.commands: # check tab-specific commands + self.commands[command][0](arg) + else: + self.core.information(_("Unknown command (%s)") % (command), _('Error')) + else: + if txt.startswith('//'): + txt = txt[1:] + self.command_say(txt) + + def command_say(self, line): + raise NotImplementedError + +class MucTab(ChatTab): + """ + The tab containing a multi-user-chat room. + It contains an userlist, an input, a topic, an information and a chat zone + """ + def __init__(self, core, room): + ChatTab.__init__(self, core, room) + self.topic_win = windows.Topic() + self.text_win = windows.TextWin() + self.v_separator = windows.VerticalSeparator() + self.user_win = windows.UserList() + self.info_header = windows.MucInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + self.ignores = [] # set of Users + # keys + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + # commands + self.commands['ignore'] = (self.command_ignore, _("Usage: /ignore \nIgnore: Ignore a specified nickname."), None) + self.commands['unignore'] = (self.command_unignore, _("Usage: /unignore \nUnignore: Remove the specified nickname from the ignore list."), None) + self.commands['kick'] = (self.command_kick, _("Usage: /kick [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason."), None) + self.commands['topic'] = (self.command_topic, _("Usage: /topic \nTopic: Change the subject of the room"), None) + self.commands['query'] = (self.command_query, _('Usage: /query [message]\nQuery: Open a private conversation with . 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]\n Part: disconnect from a room. You can specify an optional message."), None) + self.commands['nick'] = (self.command_nick, _("Usage: /nick \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.resize() + + def command_recolor(self, arg): + """ + Re-assign color to the participants of the room + """ + room = self.get_room() + i = 0 + compare_users = lambda x: x.last_talked + users = list(room.users) + # search our own user, to remove it from the room + for user in users: + if user.nick == room.own_nick: + users.remove(user) + nb_color = len(theme.LIST_COLOR_NICKNAMES) + for user in sorted(users, key=compare_users, reverse=True): + user.color = theme.LIST_COLOR_NICKNAMES[i % nb_color] + i+= 1 + self.core.refresh_window() + + def command_nick(self, arg): + """ + /nick + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + return + nick = args[0] + room = self.get_room() + if not room.joined: + return + muc.change_nick(self.core.xmpp, room.name, nick) + + def command_part(self, arg): + """ + /part [msg] + """ + args = arg.split() + reason = None + 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) + self.core.close_tab() + + def command_query(self, arg): + """ + /query [message] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) < 1: + return + nick = args[0] + room = self.get_room() + r = None + for user in room.users: + if user.nick == nick: + r = self.core.open_private_window(room.name, user.nick) + if r and len(args) > 1: + msg = arg[len(nick)+1:] + muc.send_private_message(self.core.xmpp, r.name, msg) + self.core.add_message_to_text_buffer(r, msg, None, r.own_nick) + + def command_topic(self, arg): + """ + /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) + return + subject = arg + muc.change_subject(self.core.xmpp, self.get_room().name, subject) + + def command_kick(self, arg): + """ + /kick [reason] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) < 1: + self.core.command_help('kick') + return + nick = args[0] + if len(args) >= 2: + reason = ' '.join(args[1:]) + else: + reason = '' + if not self.get_room().joined: + return + res = muc.eject_user(self.core.xmpp, self.get_name(), nick, reason) + if res['type'] == 'error': + self.core.room_error(res, self.get_name()) + + def command_say(self, line): + muc.send_groupchat_message(self.core.xmpp, self.get_name(), line) + + def command_ignore(self, arg): + """ + /ignore + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + self.core.command_help('ignore') + return + nick = args[0] + user = self._room.get_user_by_name(nick) + if not user: + self.core.information(_('%s is not in the room') % nick) + elif user in self.ignores: + self.core.information(_('%s is already ignored') % nick) + else: + self.ignores.append(user) + self.core.information(_("%s is now ignored") % nick, 'info') + + def command_unignore(self, arg): + """ + /unignore + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.core.information(str(error), _("Error")) + if len(args) != 1: + self.core.command_help('unignore') + return + nick = args[0] + user = self._room.get_user_by_name(nick) + if not user: + self.core.information(_('%s is not in the room') % nick) + elif user not in self.ignores: + self.core.information(_('%s is not ignored') % nick) + else: + self.ignores.remove(user) + self.core.information(_('%s is now unignored') % nick) + + def resize(self): + """ + Resize the whole window. i.e. all its sub-windows + """ + Tab.resize(self) + text_width = (self.width//10)*9 + self.topic_win.resize(1, self.width, 0, 0, self.core.stdscr) + self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) + self.v_separator.resize(self.height-3, 1, 1, 9*(self.width//10), self.core.stdscr) + self.user_win.resize(self.height-3, self.width-text_width-1, 1, text_width+1, self.core.stdscr) + self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.topic_win.refresh(self._room.topic) + self.text_win.refresh(self._room) + self.v_separator.refresh() + self.user_win.refresh(self._room.users) + self.info_header.refresh(self._room) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def completion(self): + """ + Called when Tab is pressed, complete the nickname in the input + """ + if self.complete_commands(self.input): + return + compare_users = lambda x: x.last_talked + word_list = [user.nick for user in sorted(self._room.users, key=compare_users, reverse=True)] + after = config.get('after_completion', ',')+" " + if ' ' not in self.input.get_text() or (self.input.last_completion and\ + self.input.get_text()[:-len(after)] == self.input.last_completion): + add_after = after + else: + add_after = ' ' + self.input.auto_completion(word_list, add_after) + + def get_color_state(self): + return self._room.color_state + + def set_color_state(self, color): + self._room.set_color_state(color) + + def get_name(self): + return self._room.name + + def get_room(self): + return self._room + + def on_lose_focus(self): + self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self._room.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + text_width = (self.width//10)*9 + self.text_win.resize(self.height-4-self.core.information_win_size, text_width, 1, 0, self.core.stdscr) + self.info_header.resize(1, (self.width//10)*9, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, (self.width//10)*9, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def just_before_refresh(self): + return + + def on_close(self): + return + +class PrivateTab(ChatTab): + """ + The tab containg a private conversation (someone from a MUC) + """ + def __init__(self, core, room): + ChatTab.__init__(self, core, room) + self.text_win = windows.TextWin() + self.info_header = windows.PrivateInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + # keys + self.key_func['^I'] = self.completion + self.key_func['M-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.resize() + + def completion(self): + self.complete_commands(self.input) + + def command_say(self, line): + muc.send_private_message(self.core.xmpp, self.get_name(), line) + self.core.add_message_to_text_buffer(self.get_room(), line, None, self.get_room().own_nick) + + def command_unquery(self, arg): + """ + /unquery + """ + self.core.close_tab() + + def resize(self): + Tab.resize(self) + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, _): + if not self.visible: + return + self.text_win.refresh(self._room) + self.info_header.refresh(self._room) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_color_state(self): + if self._room.color_state == theme.COLOR_TAB_NORMAL or\ + self._room.color_state == theme.COLOR_TAB_CURRENT: + return self._room.color_state + return theme.COLOR_TAB_PRIVATE + + def set_color_state(self, color): + self._room.color_state = color + + def get_name(self): + return self._room.name + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def on_lose_focus(self): + self._room.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self._room.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def get_room(self): + return self._room + + def just_before_refresh(self): + return + + def on_close(self): + return + +class RosterInfoTab(Tab): + """ + A tab, splitted in two, containing the roster and infos + """ + def __init__(self, core): + Tab.__init__(self, core) + self.name = "Roster" + self.v_separator = windows.VerticalSeparator() + self.tab_win = windows.GlobalInfoBar() + self.info_win = windows.TextWin() + self.roster_win = windows.RosterWin() + 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(theme.COLOR_TAB_NORMAL) + self.key_func['^I'] = self.completion + self.key_func['M-i'] = self.completion + self.key_func["^J"] = self.on_enter + self.key_func["^M"] = self.on_enter + self.key_func[' '] = self.on_space + self.key_func["/"] = self.on_slash + self.key_func["KEY_UP"] = self.move_cursor_up + self.key_func["KEY_DOWN"] = self.move_cursor_down + self.key_func["o"] = self.toggle_offline_show + self.key_func["^F"] = self.start_search + self.resize() + + def resize(self): + Tab.resize(self) + roster_width = self.width//2 + info_width = self.width-roster_width-1 + self.v_separator.resize(self.height-2, 1, 0, roster_width, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.info_win.resize(self.height-2, info_width, 0, roster_width+1, self.core.stdscr) + self.roster_win.resize(self.height-2-3, roster_width, 0, 0, self.core.stdscr) + self.contact_info_win.resize(3, roster_width, self.height-2-3, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def completion(self): + # Check if we are entering a command (with the '/' key) + if isinstance(self.input, windows.CommandInput) and\ + not self.input.help_message: + self.complete_commands(self.input) + + def refresh(self, tabs, informations, roster): + if not self.visible: + return + self.v_separator.refresh() + self.roster_win.refresh(roster) + self.contact_info_win.refresh(self.roster_win.get_selected_row()) + self.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + 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): + res = self.input.do_command(key) + if res: + return True + if key in self.key_func: + return self.key_func[key]() + + def toggle_offline_show(self): + """ + Show or hide offline contacts + """ + option = 'roster_show_offline' + if config.get(option, 'false') == 'false': + config.set_and_save(option, 'true') + else: + config.set_and_save(option, 'false') + return True + + def on_slash(self): + """ + '/' is pressed, we enter "input mode" + """ + curses.curs_set(1) + self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + self.input.do_command("/") # we add the slash + + def reset_help_message(self, _=None): + curses.curs_set(0) + self.input = self.default_help_message + return True + + def execute_slash_command(self, txt): + if txt.startswith('/'): + self.core.execute(txt) + return self.reset_help_message() + + def on_lose_focus(self): + self._color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self._color_state = theme.COLOR_TAB_CURRENT + curses.curs_set(0) + + def add_message(self): + return False + + def move_cursor_down(self): + self.roster_win.move_cursor_down() + return True + + def move_cursor_up(self): + self.roster_win.move_cursor_up() + return True + + def on_scroll_down(self): + # Scroll info win + pass + + def on_scroll_up(self): + # Scroll info down + pass + + def on_info_win_size_changed(self): + pass + + def on_space(self): + selected_row = self.roster_win.get_selected_row() + if isinstance(selected_row, RosterGroup) or\ + isinstance(selected_row, Contact): + selected_row.toggle_folded() + return True + + def on_enter(self): + selected_row = self.roster_win.get_selected_row() + self.core.on_roster_enter_key(selected_row) + return selected_row + + def start_search(self): + """ + Start the search. The input should appear with a short instruction + in it. + """ + curses.curs_set(1) + self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + return True + + def set_roster_filter(self, txt): + roster._contact_filter = (jid_and_name_match, txt) + self.roster_win.refresh(roster) + return False + + def on_search_terminate(self, txt): + curses.curs_set(0) + roster._contact_filter = None + self.reset_help_message() + return True + + def just_before_refresh(self): + return + + def on_close(self): + return + +class ConversationTab(ChatTab): + """ + The tab containg a normal conversation (someone from our roster) + """ + def __init__(self, core, text_buffer, jid): + ChatTab.__init__(self, core, text_buffer) + self.color_state = theme.COLOR_TAB_NORMAL + self._name = jid # a conversation tab is linked to one specific full jid OR bare jid + self.text_win = windows.TextWin() + self.upper_bar = windows.ConversationStatusMessageWin() + self.info_header = windows.ConversationInfoWin() + self.info_win = windows.TextWin() + self.tab_win = windows.GlobalInfoBar() + self.input = windows.MessageInput() + # keys + self.key_func['^I'] = self.completion + self.key_func['M-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.resize() + + def completion(self): + self.complete_commands(self.input) + + def command_say(self, line): + muc.send_private_message(self.core.xmpp, self.get_name(), line) + self.core.add_message_to_text_buffer(self.get_room(), line, None, self.core.own_nick) + + def command_unquery(self, arg): + """ + /unquery + """ + self.core.close_tab() + + def resize(self): + Tab.resize(self) + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 1, 0, self.core.stdscr) + self.upper_bar.resize(1, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def refresh(self, tabs, informations, roster): + if not self.visible: + return + self.text_win.refresh(self._room) + 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.info_win.refresh(informations) + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def get_color_state(self): + if self.color_state == theme.COLOR_TAB_NORMAL or\ + self.color_state == theme.COLOR_TAB_CURRENT: + return self.color_state + return theme.COLOR_TAB_PRIVATE + + def set_color_state(self, color): + self.color_state = color + + def get_name(self): + return self._name + + def on_input(self, key): + if key in self.key_func: + self.key_func[key]() + return False + self.input.do_command(key) + return False + + def on_lose_focus(self): + self.set_color_state(theme.COLOR_TAB_NORMAL) + self._room.remove_line_separator() + self._room.add_line_separator() + + def on_gain_focus(self): + self.set_color_state(theme.COLOR_TAB_CURRENT) + curses.curs_set(1) + + def on_scroll_up(self): + self._room.scroll_up(self.text_win.height-1) + + def on_scroll_down(self): + self._room.scroll_down(self.text_win.height-1) + + def on_info_win_size_changed(self): + self.text_win.resize(self.height-3-self.core.information_win_size, self.width, 0, 0, self.core.stdscr) + self.info_header.resize(1, self.width, self.height-3-self.core.information_win_size, 0, self.core.stdscr) + self.info_win.resize(self.core.information_win_size, self.width, self.height-2-self.core.information_win_size, 0, self.core.stdscr) + + def get_room(self): + return self._room + + def just_before_refresh(self): + return + + def on_close(self): + return + +class MucListTab(Tab): + """ + A tab listing rooms from a specific server, displaying various information, + scrollable, and letting the user join them, etc + """ + def __init__(self, core, server): + Tab.__init__(self, core) + self._color_state = theme.COLOR_TAB_NORMAL + self.name = server + self.upper_message = windows.Topic() + columns = ('node-part','name', 'users') + self.list_header = windows.ColumnHeaderWin(columns) + self.listview = windows.ListWin(columns) + self.tab_win = windows.GlobalInfoBar() + self.default_help_message = windows.HelpText("“j”: join room. “i”: information") + self.input = self.default_help_message + self.key_func["KEY_DOWN"] = self.listview.move_cursor_down + self.key_func["KEY_UP"] = self.listview.move_cursor_up + self.key_func["/"] = self.on_slash + self.key_func['j'] = self.join_selected + self.key_func['J'] = self.join_selected_no_focus + self.resize() + + def refresh(self, tabs, informations, roster): + self.upper_message.refresh('Chatroom list on server %s' % self.name) + self.list_header.refresh() + self.listview.refresh() + self.tab_win.refresh(tabs, tabs[0]) + self.input.refresh() + + def resize(self): + Tab.resize(self) + self.upper_message.resize(1, self.width, 0, 0, self.core.stdscr) + column_size = {'node-part': (self.width-5)//4, + 'name': (self.width-5)//4*3, + 'users': 5} + self.list_header.resize_columns(column_size) + self.list_header.resize(1, self.width, 1, 0, self.core.stdscr) + self.listview.resize_columns(column_size) + self.listview.resize(self.height-4, self.width, 2, 0, self.core.stdscr) + self.tab_win.resize(1, self.width, self.height-2, 0, self.core.stdscr) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + + def on_slash(self): + """ + '/' is pressed, activate the input + """ + curses.curs_set(1) + self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command) + self.input.resize(1, self.width, self.height-1, 0, self.core.stdscr) + self.input.do_command("/") # we add the slash + + def join_selected_no_focus(self): + return + + def join_selected(self): + jid = self.listview.get_selected_row()['jid'] + self.core.command_join(jid) + + def reset_help_message(self, _=None): + curses.curs_set(0) + self.input = self.default_help_message + return True + + def execute_slash_command(self, txt): + if txt.startswith('/'): + self.core.execute(txt) + return self.reset_help_message() + + def get_color_state(self): + return theme.COLOR_TAB_NORMAL + + def set_color_state(self, color): + pass + + def get_name(self): + return self.name + + def on_input(self, key): + res = self.input.do_command(key) + if res: + return True + if key in self.key_func: + return self.key_func[key]() + + def on_lose_focus(self): + self._color_state = theme.COLOR_TAB_NORMAL + + def on_gain_focus(self): + self._color_state = theme.COLOR_TAB_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 + be 'almost' found INSIDE a string. + 'almost' being defined by difflib + """ + l = len(search) + ratio = 0.7 + for i in range(len(string) - l + 1): + if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio: + return True + return False + +def jid_and_name_match(contact, txt): + """ + A function used to know if a contact in the roster should + be shown in the roster + """ + ratio = 0.7 + if not txt: + return True # Everything matches when search is empty + user = JID(contact.get_bare_jid()).user + if diffmatch(txt, user): + return True + if contact.get_name() and diffmatch(txt, contact.get_name()): + return True + return False diff --git a/src/windows.py b/src/windows.py index 0fa8505e..65796e5d 100644 --- a/src/windows.py +++ b/src/windows.py @@ -41,7 +41,7 @@ from contact import Contact, Resource from roster import RosterGroup, roster from message import Line -from tab import MIN_WIDTH, MIN_HEIGHT +from tabs import MIN_WIDTH, MIN_HEIGHT from sleekxmpp.xmlstream.stanzabase import JID @@ -55,17 +55,7 @@ class Win(object): def _resize(self, height, width, y, x, parent_win): self.height, self.width, self.x, self.y = height, width, x, y - # try: self._win = curses.newwin(height, width, y, x) - # except: - # # When resizing in a too little height (less than 3 lines) - # # We don't need to resize the window, since this size - # # just makes no sense - # # Just don't crash when this happens. - # # (°> also, a penguin - # # //\ - # # V_/_ - # return def _refresh(self): self._win.noutrefresh() @@ -404,10 +394,6 @@ class MucInfoWin(InfoWin): self.addstr(txt, curses.color_pair(theme.COLOR_INFORMATION_BAR)) class TextWin(Win): - """ - Just keep ONE single window for the text area and rewrite EVERYTHING - on each change. (thanks weechat :o) - """ def __init__(self): Win.__init__(self) @@ -1278,3 +1264,122 @@ class ContactInfoWin(Win): elif isinstance(selected_row, Resource): self.draw_contact_info(selected_row) self._refresh() + +class ListWin(Win): + """ + A list (with no depth, so not for the roster) that can be + scrolled up and down, with one selected line at a time + """ + def __init__(self, columns, with_headers=True): + self._columns = columns # a tuple with the name of the columns + self._columns_sizes = {} # a dict {'column_name': size} + self.sorted_by = (None, None) # for example: ('name', '↑') + self.lines = [] # a list of dicts + self._selected_row = 0 + self._starting_pos = 0 # The column number from which we start the refresh + + def resize(self, height, width, y, x, stdscr): + self._resize(height, width, y, x, stdscr) + + def resize_columns(self, dic): + """ + Resize the width of the columns + """ + self._columns_sizes = dic + + def sort_by_column(self, col_name, asc=True): + """ + Sort the list by the given column, ascendant or descendant + """ + pass # TODO + + def add_lines(self, lines): + """ + Append some lines at the end of the list + """ + if not lines: + return + self.lines += lines + self.refresh() + + def get_selected_row(self): + """ + Return the tuple representing the selected row + """ + if self._selected_row: + return self.lines[self._selected_row] + return None + + def refresh(self): + with g_lock: + self._win.erase() + lines = self.lines[self._starting_pos:self._starting_pos+self.height] + for y, line in enumerate(lines): + x = 0 + for col in self._columns: + try: + txt = line[col] or '' + except (KeyError): + txt = '' + size = self._columns_sizes[col] + txt += ' ' * (size-len(txt)) + if not txt: + continue + if line is self.lines[self._selected_row]: + self.addstr(y, x, txt[:size], curses.color_pair(theme.COLOR_INFORMATION_BAR)) + else: + self.addstr(y, x, txt[:size]) + x += size + self._refresh() + + def move_cursor_down(self): + """ + Move the cursor Down + """ + if not self.lines: + return + if self._selected_row < len(self.lines) - 1: + self._selected_row += 1 + while self._selected_row >= self._starting_pos + self.height: + self._starting_pos += self.height // 2 + if self._starting_pos < 0: + self._starting_pos = 0 + return True + + def move_cursor_up(self): + """ + Move the cursor Up + """ + if not self.lines: + return + if self._selected_row > 0: + self._selected_row -= 1 + while self._selected_row < self._starting_pos: + self._starting_pos -= self.height // 2 + return True + +class ColumnHeaderWin(Win): + """ + A class displaying the column's names + """ + def __init__(self, columns): + self._columns = columns + self._columns_sizes = {} + + def resize_columns(self, dic): + self._columns_sizes = dic + + def resize(self, height, width, y, x, stdscr): + self._resize(height, width, y, x, stdscr) + + def refresh(self): + with g_lock: + self._win.erase() + x = 0 + for col in self._columns: + txt = col + size = self._columns_sizes[col] + txt += ' ' * (size-len(txt)) + self.addstr(0, x, txt, curses.color_pair(theme.COLOR_STATUS_UNAVAILABLE)) + x += size + self._refresh() -- cgit v1.2.3