Commit 2f629ee6 authored by mathieui's avatar mathieui

Split the windows.py module into a subdirectory

parent 109e86cb
This diff is collapsed.
"""
Module exporting all the Windows, which are wrappers around curses wins
used to display information on the screen
"""
from . base_wins import Win, g_lock
from . info_bar import GlobalInfoBar, VerticalGlobalInfoBar
from . info_wins import InfoWin, XMLInfoWin, PrivateInfoWin, MucListInfoWin, \
ConversationInfoWin, DynamicConversationInfoWin, MucInfoWin, \
ConversationStatusMessageWin
from . input_placeholders import HelpText, YesNoInput
from . inputs import Input, HistoryInput, MessageInput, CommandInput
from . list import ListWin, ColumnHeaderWin
from . misc import VerticalSeparator
from . muc import UserList, Topic
from . roster_win import RosterWin, ContactInfoWin
from . text_win import TextWin
"""
Define the base window object and the constants/"globals" used
by the file of this module.
A window is a little part of the screen, for example the input window,
the text window, the roster window, etc.
A Tab (see the src/tabs module) is composed of multiple Windows
"""
import logging
log = logging.getLogger(__name__)
import collections
import curses
import string
from threading import RLock
import core
import singleton
from theming import to_curses_attr, read_tuple
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
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
"""
Standalone functions used by the modules
"""
import string
from config import config
from . base_wins import FORMAT_CHAR, format_chars
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
"""
Module defining the global info bar
This window is the one listing the current opened tabs in poezio.
The GlobalInfoBar can be either horizontal or vertical
(VerticalGlobalInfoBar).
"""
import logging
log = logging.getLogger(__name__)
import curses
from config import config
from . import Win, g_lock
from theming import get_theme, to_curses_attr
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()
"""
Module defining all the "info wins", ie the bar which is on top of the
info buffer in normal tabs
"""
import logging
log = logging.getLogger(__name__)
from common import safeJID
from . import Win, g_lock
from . funcs import truncate_nick
from theming import get_theme, to_curses_attr
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))
class MucInfoWin(InfoWin):
"""
The line just above the information window, displaying informations
about the MUC we are viewing
"""
def __init__(self):
InfoWin.__init__(self)
def refresh(self, room, window=None):
log.debug('Refresh: %s', self.__class__.__name__)
with g_lock:
self._win.erase()
self.write_room_name(room)
self.write_participants_number(room)
self.write_own_nick(room)
self.write_disconnected(room)
self.write_role(room)
if window:
self.print_scroll_position(window)
self.finish_line(get_theme().COLOR_INFORMATION_BAR)
self._refresh()
def write_room_name(self, room):
self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(room.name, to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_participants_number(self, room):
self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
self.addstr(str(len(room.users)), to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_disconnected(self, room):
"""
Shows a message if the room is not joined
"""
if not room.joined:
self.addstr(' -!- Not connected ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_own_nick(self, room):
"""
Write our own nick in the info bar
"""
nick = room.own_nick
if not nick:
return
self.addstr(truncate_nick(nick, 13), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_role(self, room):
"""
Write our own role and affiliation
"""
own_user = None
for user in room.users:
if user.nick == room.own_nick:
own_user = user
break
if not own_user:
return
txt = ' ('
if own_user.affiliation != 'none':