Commit 2f629ee6 authored by mathieui's avatar mathieui

Split the windows.py module into a subdirectory

parent 109e86cb
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the zlib license. See the COPYING file.
"""
Define all the windows.
A window is a little part of the screen, for example the input window,
the text window, the roster window, etc.
A Tab (see tab.py) is composed of multiple Windows
"""
import logging
log = logging.getLogger(__name__)
import collections
import curses
import string
from datetime import datetime
from math import ceil, log10
from threading import RLock
import common
import core
import poopt
import singleton
from common import safeJID
from config import config
from contact import Contact, Resource
from roster import RosterGroup
from theming import get_theme, to_curses_attr, read_tuple, dump_tuple
from user import ROLE_DICT
FORMAT_CHAR = '\x19'
# These are non-printable chars, so they should never appear in the input,
# I guess. But maybe we can find better chars that are even less risky.
format_chars = ['\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13',
'\x14', '\x15', '\x16', '\x17', '\x18']
# different colors allowed in the input
allowed_color_digits = ('0', '1', '2', '3', '4', '5', '6', '7')
# msg is a reference to the corresponding Message tuple. text_start and
# text_end are the position delimiting the text in this line.
Line = collections.namedtuple('Line', 'msg start_pos end_pos prepend')
g_lock = RLock()
LINES_NB_LIMIT = 4096
class DummyWin(object):
def __getattribute__(self, name):
if name != '__bool__':
return lambda *args, **kwargs: (0, 0)
else:
return object.__getattribute__(self, name)
def __bool__(self):
return False
def find_first_format_char(text, chars=None):
if chars is None:
chars = format_chars
pos = -1
for char in chars:
p = text.find(char)
if p == -1:
continue
if pos == -1 or p < pos:
pos = p
return pos
def truncate_nick(nick, size=None):
size = size or config.get('max_nick_length', 25)
if size < 1:
size = 1
if nick and len(nick) > size:
return nick[:size]+'…'
return nick
def parse_attrs(text, previous=None):
next_attr_char = text.find(FORMAT_CHAR)
if previous:
attrs = previous
else:
attrs = []
while next_attr_char != -1 and text:
if next_attr_char + 1 < len(text):
attr_char = text[next_attr_char+1].lower()
else:
attr_char = str()
if attr_char == 'o':
attrs = []
elif attr_char == 'u':
attrs.append('u')
elif attr_char == 'b':
attrs.append('b')
if attr_char in string.digits and attr_char != '':
color_str = text[next_attr_char+1:text.find('}', next_attr_char)]
if color_str:
attrs.append(color_str + '}')
text = text[next_attr_char+len(color_str)+2:]
else:
text = text[next_attr_char+2:]
next_attr_char = text.find(FORMAT_CHAR)
return attrs
class Win(object):
_win_core = None
_tab_win = None
def __init__(self):
self._win = None
self.height, self.width = 0, 0
def _resize(self, height, width, y, x):
if height == 0 or width == 0:
self.height, self.width = height, width
return
self.height, self.width, self.x, self.y = height, width, x, y
try:
self._win = Win._tab_win.derwin(height, width, y, x)
except:
log.debug('DEBUG: mvwin returned ERR. Please investigate')
if self._win is None:
self._win = DummyWin()
def resize(self, height, width, y, x):
"""
Override if something has to be done on resize
"""
with g_lock:
self._resize(height, width, y, x)
def _refresh(self):
self._win.noutrefresh()
def addnstr(self, *args):
"""
Safe call to addnstr
"""
try:
self._win.addnstr(*args)
except:
# this actually mostly returns ERR, but works.
# more specifically, when the added string reaches the end
# of the screen.
pass
def addstr(self, *args):
"""
Safe call to addstr
"""
try:
self._win.addstr(*args)
except:
pass
def move(self, y, x):
try:
self._win.move(y, x)
except:
self._win.move(0, 0)
def addstr_colored(self, text, y=None, x=None):
"""
Write a string on the window, setting the
attributes as they are in the string.
For example:
\x19bhello → hello in bold
\x191}Bonj\x192}our → 'Bonj' in red and 'our' in green
next_attr_char is the \x19 delimiter
attr_char is the char following it, it can be
one of 'u', 'b', 'c[0-9]'
"""
if y is not None and x is not None:
self.move(y, x)
next_attr_char = text.find(FORMAT_CHAR)
while next_attr_char != -1 and text:
if next_attr_char + 1 < len(text):
attr_char = text[next_attr_char+1].lower()
else:
attr_char = str()
if next_attr_char != 0:
self.addstr(text[:next_attr_char])
if attr_char == 'o':
self._win.attrset(0)
elif attr_char == 'u':
self._win.attron(curses.A_UNDERLINE)
elif attr_char == 'b':
self._win.attron(curses.A_BOLD)
if (attr_char in string.digits or attr_char == '-') and attr_char != '':
color_str = text[next_attr_char+1:text.find('}', next_attr_char)]
if ',' in color_str:
tup, char = read_tuple(color_str)
self._win.attron(to_curses_attr(tup))
if char:
if char == 'o':
self._win.attrset(0)
elif char == 'u':
self._win.attron(curses.A_UNDERLINE)
elif char == 'b':
self._win.attron(curses.A_BOLD)
elif color_str:
self._win.attron(to_curses_attr((int(color_str), -1)))
text = text[next_attr_char+len(color_str)+2:]
else:
text = text[next_attr_char+2:]
next_attr_char = text.find(FORMAT_CHAR)
self.addstr(text)
def finish_line(self, color=None):
"""
Write colored spaces until the end of line
"""
(y, x) = self._win.getyx()
size = self.width - x
if color:
self.addnstr(' '*size, size, to_curses_attr(color))
else:
self.addnstr(' '*size, size)
@property
def core(self):
if not Win._win_core:
Win._win_core = singleton.Singleton(core.Core)
return Win._win_core
class UserList(Win):
def __init__(self):
Win.__init__(self)
self.pos = 0
def scroll_up(self):
self.pos += self.height-1
return True
def scroll_down(self):
pos = self.pos
self.pos -= self.height-1
if self.pos < 0:
self.pos = 0
return self.pos != pos
def draw_plus(self, y):
self.addstr(y, self.width-2, '++', to_curses_attr(get_theme().COLOR_MORE_INDICATOR))
def refresh(self, users):
log.debug('Refresh: %s', self.__class__.__name__)
if config.get("hide_user_list", False):
return # do not refresh if this win is hidden.
with g_lock:
self._win.erase()
if config.get('user_list_sort', 'desc').lower() == 'asc':
y, x = self._win.getmaxyx()
y -= 1
users = sorted(users)
else:
y = 0
users = sorted(users)
if len(users) < self.height:
self.pos = 0
elif self.pos >= len(users) - self.height and self.pos != 0:
self.pos = len(users) - self.height
for user in users[self.pos:]:
self.draw_role_affiliation(y, user)
self.draw_status_chatstate(y, user)
self.addstr(y, 2,
poopt.cut_by_columns(user.nick, self.width - 2),
to_curses_attr(user.color))
if config.get('user_list_sort', 'desc').lower() == 'asc':
y -= 1
else:
y += 1
if y == self.height:
break
# draw indicators of position in the list
if self.pos > 0:
if config.get('user_list_sort', 'desc').lower() == 'asc':
self.draw_plus(self.height-1)
else:
self.draw_plus(0)
if self.pos + self.height < len(users):
if config.get('user_list_sort', 'desc').lower() == 'asc':
self.draw_plus(0)
else:
self.draw_plus(self.height-1)
self._refresh()
def draw_role_affiliation(self, y, user):
theme = get_theme()
color = theme.color_role(user.role)
symbol = theme.char_affiliation(user.affiliation)
self.addstr(y, 1, symbol, to_curses_attr(color))
def draw_status_chatstate(self, y, user):
show_col = get_theme().color_show(user.show)
if user.chatstate == 'composing':
char = get_theme().CHAR_CHATSTATE_COMPOSING
elif user.chatstate == 'active':
char = get_theme().CHAR_CHATSTATE_ACTIVE
elif user.chatstate == 'paused':
char = get_theme().CHAR_CHATSTATE_PAUSED
else:
char = get_theme().CHAR_STATUS
self.addstr(y, 0, char, to_curses_attr(show_col))
def resize(self, height, width, y, x):
with g_lock:
separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)
self._resize(height, width, y, x)
self._win.attron(separator)
self._win.vline(0, 0, curses.ACS_VLINE, self.height)
self._win.attroff(separator)
class Topic(Win):
def __init__(self):
Win.__init__(self)
self._message = ''
def refresh(self, topic=None):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
if topic:
msg = topic[:self.width-1]
else:
msg = self._message[:self.width-1]
self.addstr(0, 0, msg, to_curses_attr(get_theme().COLOR_TOPIC_BAR))
(y, x) = self._win.getyx()
remaining_size = self.width - x
if remaining_size:
self.addnstr(' '*remaining_size, remaining_size,
to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self._refresh()
def set_message(self, message):
self._message = message
class GlobalInfoBar(Win):
def __init__(self):
Win.__init__(self)
def refresh(self):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
self.addstr(0, 0, "[", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
create_gaps = config.get('create_gaps', False)
show_names = config.get('show_tab_names', False)
show_nums = config.get('show_tab_numbers', True)
use_nicks = config.get('use_tab_nicks', True)
# ignore any remaining gap tabs if the feature is not enabled
if create_gaps:
sorted_tabs = self.core.tabs[:]
else:
sorted_tabs = [tab for tab in self.core.tabs if tab]
for nb, tab in enumerate(sorted_tabs):
if not tab: continue
color = tab.color
if not config.get('show_inactive_tabs', True) and\
color is get_theme().COLOR_TAB_NORMAL:
continue
try:
if show_nums or not show_names:
self.addstr("%s" % str(nb), to_curses_attr(color))
if show_names:
self.addstr(' ', to_curses_attr(color))
if show_names:
if use_nicks:
self.addstr("%s" % str(tab.get_nick()), to_curses_attr(color))
else:
self.addstr("%s" % tab.name, to_curses_attr(color))
self.addstr("|", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
except: # end of line
break
(y, x) = self._win.getyx()
self.addstr(y, x-1, '] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
(y, x) = self._win.getyx()
remaining_size = self.width - x
self.addnstr(' '*remaining_size, remaining_size,
to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self._refresh()
class VerticalGlobalInfoBar(Win):
def __init__(self, scr):
Win.__init__(self)
self._win = scr
def refresh(self):
with g_lock:
height, width = self._win.getmaxyx()
self._win.erase()
sorted_tabs = [tab for tab in self.core.tabs if tab]
if not config.get('show_inactive_tabs', True):
sorted_tabs = [tab for tab in sorted_tabs if\
tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL]
nb_tabs = len(sorted_tabs)
use_nicks = config.get('use_tab_nicks', True)
if nb_tabs >= height:
for y, tab in enumerate(sorted_tabs):
if tab.vertical_color == get_theme().COLOR_VERTICAL_TAB_CURRENT:
pos = y
break
# center the current tab as much as possible
if pos < height//2:
sorted_tabs = sorted_tabs[:height]
elif nb_tabs - pos <= height//2:
sorted_tabs = sorted_tabs[-height:]
else:
sorted_tabs = sorted_tabs[pos-height//2 : pos+height//2]
for y, tab in enumerate(sorted_tabs):
color = tab.vertical_color
if not config.get('vertical_tab_list_sort', 'desc') != 'asc':
y = height - y - 1
self.addstr(y, 0, "%2d" % tab.nb,
to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER))
self.addstr('.')
if use_nicks:
self.addnstr("%s" % tab.get_nick(), width - 4, to_curses_attr(color))
else:
self.addnstr("%s" % tab.name, width - 4, to_curses_attr(color))
separator = to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)
self._win.attron(separator)
self._win.vline(0, width-1, curses.ACS_VLINE, height)
self._win.attroff(separator)
self._refresh()
class InfoWin(Win):
"""
Base class for all the *InfoWin, used in various tabs. For example
MucInfoWin, etc. Provides some useful methods.
"""
def __init__(self):
Win.__init__(self)
def print_scroll_position(self, window):
"""
Print, like in Weechat, a -MORE(n)- where n
is the number of available lines to scroll
down
"""
if window.pos > 0:
plus = ' -MORE(%s)-' % window.pos
self.addstr(plus, to_curses_attr(get_theme().COLOR_SCROLLABLE_NUMBER))
class XMLInfoWin(InfoWin):
"""
Info about the latest xml filter used and the state of the buffer.
"""
def __init__(self):
InfoWin.__init__(self)
def refresh(self, filter_t='', filter='', window=None):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
bar = to_curses_attr(get_theme().COLOR_INFORMATION_BAR)
if not filter_t:
self.addstr('[No filter]', bar)
else:
info = '[%s] %s' % (filter_t, filter)
self.addstr(info, bar)
self.print_scroll_position(window)
self.finish_line(get_theme().COLOR_INFORMATION_BAR)
self._refresh()
class PrivateInfoWin(InfoWin):
"""
The line above the information window, displaying informations
about the MUC user we are talking to
"""
def __init__(self):
InfoWin.__init__(self)
def refresh(self, name, window, chatstate, informations):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
self.write_room_name(name)
self.print_scroll_position(window)
self.write_chatstate(chatstate)
self.write_additional_informations(informations, name)
self.finish_line(get_theme().COLOR_INFORMATION_BAR)
self._refresh()
def write_additional_informations(self, informations, jid):
"""
Write all informations added by plugins by getting the
value returned by the callbacks.
"""
for key in informations:
self.addstr(informations[key](jid), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_room_name(self, name):
jid = safeJID(name)
room_name, nick = jid.bare, jid.resource
self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME))
txt = ' from room %s' % room_name
self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_chatstate(self, state):
if state:
self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
class MucListInfoWin(InfoWin):
"""
The live above the information window, displaying informations
about the muc server being listed
"""
def __init__(self, message=''):
InfoWin.__init__(self)
self.message = message
def refresh(self, name=None, window=None):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
if name:
self.addstr(name, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
else:
self.addstr(self.message, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
if window:
self.print_scroll_position(window)
self.finish_line(get_theme().COLOR_INFORMATION_BAR)
self._refresh()
class ConversationInfoWin(InfoWin):
"""
The line above the information window, displaying informations
about the user we are talking to
"""
def __init__(self):
InfoWin.__init__(self)
def refresh(self, jid, contact, window, chatstate, informations):
# contact can be None, if we receive a message
# from someone not in our roster. In this case, we display
# only the maximum information from the message we can get.
log.debug('Refresh: %s', self.__class__.__name__)
jid = safeJID(jid)
if contact:
if jid.resource:
resource = contact[jid.full]
else:
resource = contact.get_highest_priority_resource()
else:
resource = None
# if contact is None, then resource is None too:
# user is not in the roster so we know almost nothing about it
# If contact is a Contact, then
# resource can now be a Resource: user is in the roster and online
# or resource is None: user is in the roster but offline
with g_lock:
self._win.erase()
self.write_contact_jid(jid)
self.write_contact_informations(contact)
self.write_resource_information(resource)
self.print_scroll_position(window)
self.write_chatstate(chatstate)
self.write_additional_informations(informations, jid)
self.finish_line(get_theme().COLOR_INFORMATION_BAR)
self._refresh()
def write_additional_informations(self, informations, jid):
"""
Write all informations added by plugins by getting the
value returned by the callbacks.
"""
for key in informations:
self.addstr(informations[key](jid),
to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_resource_information(self, resource):
"""
Write the informations about the resource
"""
if not resource:
presence = "unavailable"
else:
presence = resource.presence
color = get_theme().color_show(presence)
self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(get_theme().CHAR_STATUS, to_curses_attr(color))
self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_contact_informations(self, contact):
"""
Write the informations about the contact
"""
if not contact:
self.addstr("(contact not in roster)", to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
return
display_name = contact.name
if display_name:
self.addstr('%s '%(display_name), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_contact_jid(self, jid):
"""
Just write the jid that we are talking to
"""
self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(jid.full, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME))
self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_chatstate(self, state):
if state:
self.addstr(' %s' % (state,), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
class DynamicConversationInfoWin(ConversationInfoWin):
def write_contact_jid(self, jid):
"""
Just displays the resource in an other color
"""
log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s",
jid.resource)
self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(jid.bare, to_curses_attr(get_theme().COLOR_CONVERSATION_NAME))
if jid.resource:
self.addstr("/%s" % (jid.resource,), to_curses_attr(get_theme().COLOR_CONVERSATION_RESOURCE))
self.addstr('] ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))