diff options
-rw-r--r-- | src/core.py | 126 | ||||
-rw-r--r-- | src/tabs.py (renamed from src/tab.py) | 114 | ||||
-rw-r--r-- | src/windows.py | 135 |
3 files changed, 292 insertions, 83 deletions
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 <server> + 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 <nickname> @@ -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/tabs.py index 4a76fca4..2dc6a017 100644 --- a/src/tab.py +++ b/src/tabs.py @@ -125,19 +125,19 @@ class Tab(object): raise NotImplementedError def on_input(self, key): - raise NotImplementedError + pass def on_lose_focus(self): """ called when this tab loses the focus. """ - raise NotImplementedError + pass def on_gain_focus(self): """ called when this tab gains the focus. """ - raise NotImplementedError + pass def add_message(self): """ @@ -146,25 +146,25 @@ class Tab(object): FormTab, where text is not intented to be appened), it returns False. If the tab can, it returns True """ - raise NotImplementedError + return False def on_scroll_down(self): """ Defines what happens when we scrol down """ - raise NotImplementedError + pass def on_scroll_up(self): """ Defines what happens when we scrol down """ - raise NotImplementedError + pass def on_info_win_size_changed(self): """ Called when the window with the informations is resized """ - raise NotImplementedError + pass def just_before_refresh(self): """ @@ -172,13 +172,13 @@ class Tab(object): Particularly useful to move the cursor at the correct position. """ - raise NotImplementedError + pass def on_close(self): """ Called when the tab is to be closed """ - raise NotImplementedError + pass class InfoTab(Tab): """ @@ -933,6 +933,102 @@ class ConversationTab(ChatTab): 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 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() |