summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/commands.py2
-rw-r--r--src/core/core.py2
-rw-r--r--src/core/handlers.py38
-rw-r--r--src/tabs/xmltab.py168
-rwxr-xr-xsrc/theming.py7
-rw-r--r--src/windows/__init__.py2
-rw-r--r--src/windows/text_win.py262
7 files changed, 375 insertions, 106 deletions
diff --git a/src/core/commands.py b/src/core/commands.py
index 715e213e..2777833b 100644
--- a/src/core/commands.py
+++ b/src/core/commands.py
@@ -994,11 +994,11 @@ def command_message(self, args):
@command_args_parser.ignored
def command_xml_tab(self):
"""/xml_tab"""
- self.xml_tab = True
xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab)
if not xml_tab:
tab = tabs.XMLTab()
self.add_tab(tab, True)
+ self.xml_tab = tab
@command_args_parser.quoted(1)
def command_adhoc(self, args):
diff --git a/src/core/core.py b/src/core/core.py
index 54d48aa4..79d19087 100644
--- a/src/core/core.py
+++ b/src/core/core.py
@@ -90,7 +90,7 @@ class Core(object):
self.tab_win = windows.GlobalInfoBar()
# Whether the XML tab is opened
- self.xml_tab = False
+ self.xml_tab = None
self.xml_buffer = TextBuffer()
self.tabs = []
diff --git a/src/core/handlers.py b/src/core/handlers.py
index dc6238d5..e0c89abf 100644
--- a/src/core/handlers.py
+++ b/src/core/handlers.py
@@ -17,7 +17,8 @@ from os import path
from slixmpp import InvalidJID
from slixmpp.stanza import Message
-from slixmpp.xmlstream.stanzabase import StanzaBase
+from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
+from xml.etree import ElementTree as ET
import bookmark
import common
@@ -37,6 +38,18 @@ from theming import dump_tuple, get_theme
from . commands import dumb_callback
+try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import HtmlFormatter
+ LEXER = get_lexer_by_name('xml')
+ FORMATTER = HtmlFormatter(noclasses=True)
+except ImportError:
+ def highlight(text, *args, **kwargs):
+ return text
+ LEXER = None
+ FORMATTER = None
+
def on_session_start_features(self, _):
"""
Enable carbons & blocking on session start if wanted and possible
@@ -1104,7 +1117,17 @@ def outgoing_stanza(self, stanza):
We are sending a new stanza, write it in the xml buffer if needed.
"""
if self.xml_tab:
- self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza)
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True)
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ try:
+ if self.xml_tab.match_stanza(ElementBase(ET.fromstring(stanza))):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ except:
+ log.debug('', exc_info=True)
+
if isinstance(self.current_tab(), tabs.XMLTab):
self.current_tab().refresh()
self.doupdate()
@@ -1114,7 +1137,16 @@ def incoming_stanza(self, stanza):
We are receiving a new stanza, write it in the xml buffer if needed.
"""
if self.xml_tab:
- self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza)
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True)
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ try:
+ if self.xml_tab.match_stanza(stanza):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ except:
+ log.debug('', exc_info=True)
if isinstance(self.current_tab(), tabs.XMLTab):
self.current_tab().refresh()
self.doupdate()
diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py
index 84ecf00c..30b5b1c3 100644
--- a/src/tabs/xmltab.py
+++ b/src/tabs/xmltab.py
@@ -13,23 +13,62 @@ log = logging.getLogger(__name__)
import curses
import os
from slixmpp.xmlstream import matcher
-from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.stanzabase import ElementBase
+from xml.etree import ElementTree as ET
from . import Tab
+import text_buffer
import windows
from xhtml import clean_text
from decorators import command_args_parser
+from common import safeJID
+
+
+class MatchJID(object):
+
+ def __init__(self, jid, dest=''):
+ self.jid = jid
+ self.dest = dest
+
+ def match(self, xml):
+ from_ = safeJID(xml['from'])
+ to_ = safeJID(xml['to'])
+ if self.jid.full == self.jid.bare:
+ from_ = from_.bare
+ to_ = to_.bare
+
+ if self.dest == 'from':
+ return from_ == self.jid
+ elif self.dest == 'to':
+ return to_ == self.jid
+ return self.jid in (from_, to_)
+
+ def __repr__(self):
+ return '%s%s%s' % (self.dest, ': ' if self.dest else '', self.jid)
+
+MATCHERS_MAPPINGS = {
+ MatchJID: ('JID', lambda obj: repr(obj)),
+ matcher.MatcherId: ('ID', lambda obj: obj._criteria),
+ matcher.MatchXMLMask: ('XMLMask', lambda obj: obj._criteria),
+ matcher.MatchXPath: ('XPath', lambda obj: obj._criteria)
+}
class XMLTab(Tab):
def __init__(self):
Tab.__init__(self)
self.state = 'normal'
self.name = 'XMLTab'
- self.text_win = windows.TextWin()
- self.core.xml_buffer.add_window(self.text_win)
+ self.filters = []
+
+ self.core_buffer = self.core.xml_buffer
+ self.filtered_buffer = text_buffer.TextBuffer()
+
self.info_header = windows.XMLInfoWin()
+ self.text_win = windows.XMLTextWin()
+ self.core_buffer.add_window(self.text_win)
self.default_help_message = windows.HelpText("/ to enter a command")
+
self.register_command('close', self.close,
shortdesc=_("Close this tab."))
self.register_command('clear', self.command_clear,
@@ -42,8 +81,21 @@ class XMLTab(Tab):
shortdesc=_('Filter by id.'))
self.register_command('filter_xpath', self.command_filter_xpath,
usage='<xpath>',
- desc=_('Show only the stanzas matching the xpath <xpath>.'),
+ desc=_('Show only the stanzas matching the xpath <xpath>.'
+ ' Any occurrences of %n will be replaced by jabber:client.'),
shortdesc=_('Filter by XPath.'))
+ self.register_command('filter_jid', self.command_filter_jid,
+ usage='<jid>',
+ desc=_('Show only the stanzas matching the jid <jid> in from= or to=.'),
+ shortdesc=_('Filter by JID.'))
+ self.register_command('filter_from', self.command_filter_from,
+ usage='<jid>',
+ desc=_('Show only the stanzas matching the jid <jid> in from=.'),
+ shortdesc=_('Filter by JID from.'))
+ self.register_command('filter_to', self.command_filter_to,
+ usage='<jid>',
+ desc=_('Show only the stanzas matching the jid <jid> in to=.'),
+ shortdesc=_('Filter by JID to.'))
self.register_command('filter_xmlmask', self.command_filter_xmlmask,
usage=_('<xml mask>'),
desc=_('Show only the stanzas matching the given xml mask.'),
@@ -64,6 +116,34 @@ class XMLTab(Tab):
self.filter_type = ''
self.filter = ''
+ def gen_filter_repr(self):
+ if not self.filters:
+ self.filter_type = ''
+ self.filter = ''
+ return
+ filter_types = map(lambda x: MATCHERS_MAPPINGS[type(x)][0], self.filters)
+ filter_strings = map(lambda x: MATCHERS_MAPPINGS[type(x)][1](x), self.filters)
+ self.filter_type = ','.join(filter_types)
+ self.filter = ','.join(filter_strings)
+
+ def update_filters(self, matcher):
+ if not self.filters:
+ messages = self.core_buffer.messages[:]
+ self.filtered_buffer.messages = []
+ self.core_buffer.del_window(self.text_win)
+ self.filtered_buffer.add_window(self.text_win)
+ else:
+ messages = self.filtered_buffer.messages
+ self.filtered_buffer.messages = []
+ self.filters.append(matcher)
+ new_messages = []
+ for msg in messages:
+ if self.match_stanza(ElementBase(ET.fromstring(clean_text(msg.txt)))):
+ new_messages.append(msg)
+ self.filtered_buffer.messages = new_messages
+ self.text_win.rebuild_everything(self.filtered_buffer)
+ self.gen_filter_repr()
+
def on_freeze(self):
"""
Freeze the display.
@@ -71,46 +151,66 @@ class XMLTab(Tab):
self.text_win.toggle_lock()
self.refresh()
+ def match_stanza(self, stanza):
+ for matcher in self.filters:
+ if not matcher.match(stanza):
+ return False
+ return True
+
@command_args_parser.raw
def command_filter_xmlmask(self, mask):
"""/filter_xmlmask <xml mask>"""
try:
- handler = Callback('custom matcher', matcher.MatchXMLMask(mask),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XML Mask Filter"
- self.filter = mask
+ self.update_filters(matcher.MatchXMLMask(mask))
self.refresh()
except:
self.core.information('Invalid XML Mask', 'Error')
self.command_reset('')
+ @command_args_parser.raw
+ def command_filter_to(self, jid):
+ """/filter_jid_to <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='to'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_from(self, jid):
+ """/filter_jid_from <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='from'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_jid(self, jid):
+ """/filter_jid <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj))
+ self.refresh()
+
@command_args_parser.quoted(1)
def command_filter_id(self, args):
"""/filter_id <id>"""
if args is None:
return self.core.command_help('filter_id')
- self.core.xmpp.remove_handler('custom matcher')
- handler = Callback('custom matcher', matcher.MatcherId(arg),
- self.core.incoming_stanza)
- self.core.xmpp.register_handler(handler)
- self.filter_type = "Id Filter"
- self.filter = args[0]
+ self.update_filters(matcher.MatcherId(args[0]))
self.refresh()
@command_args_parser.raw
def command_filter_xpath(self, xpath):
"""/filter_xpath <xpath>"""
try:
- handler = Callback('custom matcher', matcher.MatchXPath(
- xpath.replace('%n', self.core.xmpp.default_ns)),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XPath Filter"
- self.filter = xpath
+ self.update_filters(matcher.MatchXPath(xpath.replace('%n', self.core.xmpp.default_ns)))
self.refresh()
except:
self.core.information('Invalid XML Path', 'Error')
@@ -119,8 +219,11 @@ class XMLTab(Tab):
@command_args_parser.ignored
def command_reset(self):
"""/reset"""
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(self.core.all_stanzas)
+ if self.filters:
+ self.filters = []
+ self.filtered_buffer.del_window(self.text_win)
+ self.core_buffer.add_window(self.text_win)
+ self.text_win.rebuild_everything(self.core_buffer)
self.filter_type = ''
self.filter = ''
self.refresh()
@@ -130,8 +233,11 @@ class XMLTab(Tab):
"""/dump <filename>"""
if args is None:
return self.core.command_help('dump')
- xml = self.core.xml_buffer.messages[:]
- text = '\n'.join(('%s %s' % (msg.str_time, clean_text(msg.txt)) for msg in xml))
+ if self.filters:
+ xml = self.filtered_buffer.messages[:]
+ else:
+ xml = self.core_buffer.messages[:]
+ text = '\n'.join(('%s %s %s' % (msg.str_time, msg.nickname, clean_text(msg.txt)) for msg in xml))
filename = os.path.expandvars(os.path.expanduser(args[0]))
try:
with open(filename, 'w') as fd:
@@ -167,8 +273,12 @@ class XMLTab(Tab):
"""
/clear
"""
- self.core.xml_buffer.messages = []
- self.text_win.rebuild_everything(self.core.xml_buffer)
+ if self.filters:
+ buffer = self.core_buffer
+ else:
+ buffer = self.filtered_buffer
+ buffer.messages = []
+ self.text_win.rebuild_everything(buffer)
self.refresh()
self.core.doupdate()
diff --git a/src/theming.py b/src/theming.py
index 1e9d6c40..3ca1a89f 100755
--- a/src/theming.py
+++ b/src/theming.py
@@ -178,6 +178,13 @@ class Theme(object):
CHAR_AFFILIATION_MEMBER = '+'
CHAR_AFFILIATION_NONE = '-'
+
+ # XML Tab
+ CHAR_XML_IN = 'IN '
+ CHAR_XML_OUT = 'OUT'
+ COLOR_XML_IN = (1, -1)
+ COLOR_XML_OUT = (2, -1)
+
# Color for the /me message
COLOR_ME_MESSAGE = (6, -1)
diff --git a/src/windows/__init__.py b/src/windows/__init__.py
index 9e165201..f9ca7108 100644
--- a/src/windows/__init__.py
+++ b/src/windows/__init__.py
@@ -15,5 +15,5 @@ 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
+from . text_win import TextWin, XMLTextWin
diff --git a/src/windows/text_win.py b/src/windows/text_win.py
index 6fe74f41..380142bb 100644
--- a/src/windows/text_win.py
+++ b/src/windows/text_win.py
@@ -18,7 +18,7 @@ from config import config
from theming import to_curses_attr, get_theme, dump_tuple
-class TextWin(Win):
+class BaseTextWin(Win):
def __init__(self, lines_nb_limit=None):
if lines_nb_limit is None:
lines_nb_limit = config.get('max_lines_in_memory')
@@ -30,19 +30,6 @@ class TextWin(Win):
self.lock = False
self.lock_buffer = []
-
- # the Lines of the highlights in that buffer
- self.highlights = []
- # the current HL position in that list NaN means that we’re not on
- # an hl. -1 is a valid position (it's before the first hl of the
- # list. i.e the separator, in the case where there’s no hl before
- # it.)
- self.hl_pos = float('nan')
-
- # Keep track of the number of hl after the separator.
- # This is useful to make “go to next highlight“ work after a “move to separator”.
- self.nb_of_highlights_after_separator = 0
-
self.separator_after = None
def toggle_lock(self):
@@ -60,6 +47,113 @@ class TextWin(Win):
self.built_lines.append(line)
self.lock = False
+ def scroll_up(self, dist=14):
+ pos = self.pos
+ self.pos += dist
+ if self.pos + self.height > len(self.built_lines):
+ self.pos = len(self.built_lines) - self.height
+ if self.pos < 0:
+ self.pos = 0
+ return self.pos != pos
+
+ def scroll_down(self, dist=14):
+ pos = self.pos
+ self.pos -= dist
+ if self.pos <= 0:
+ self.pos = 0
+ return self.pos != pos
+
+ def build_new_message(self, message, history=None, clean=True, highlight=False, timestamp=False):
+ """
+ Take one message, build it and add it to the list
+ Return the number of lines that are built for the given
+ message.
+ """
+ lines = self.build_message(message, timestamp=timestamp)
+ if self.lock:
+ self.lock_buffer.extend(lines)
+ else:
+ self.built_lines.extend(lines)
+ if not lines or not lines[0]:
+ return 0
+ if clean:
+ while len(self.built_lines) > self.lines_nb_limit:
+ self.built_lines.pop(0)
+ return len(lines)
+
+ def build_message(self, message, timestamp=False):
+ """
+ Build a list of lines from a message, without adding it
+ to a list
+ """
+ pass
+
+ def refresh(self):
+ pass
+
+ def write_text(self, y, x, txt):
+ """
+ write the text of a line.
+ """
+ self.addstr_colored(txt, y, x)
+
+ def write_time(self, time):
+ """
+ Write the date on the yth line of the window
+ """
+ if time:
+ self.addstr(time)
+ self.addstr(' ')
+
+ def resize(self, height, width, y, x, room=None):
+ if hasattr(self, 'width'):
+ old_width = self.width
+ else:
+ old_width = None
+ self._resize(height, width, y, x)
+ if room and self.width != old_width:
+ self.rebuild_everything(room)
+
+ # reposition the scrolling after resize
+ # (see #2450)
+ buf_size = len(self.built_lines)
+ if buf_size - self.pos < self.height:
+ self.pos = buf_size - self.height
+ if self.pos < 0:
+ self.pos = 0
+
+ def rebuild_everything(self, room):
+ self.built_lines = []
+ with_timestamps = config.get('show_timestamps')
+ for message in room.messages:
+ self.build_new_message(message, clean=False, timestamp=with_timestamps)
+ if self.separator_after is message:
+ self.build_new_message(None)
+ while len(self.built_lines) > self.lines_nb_limit:
+ self.built_lines.pop(0)
+
+ def __del__(self):
+ log.debug('** TextWin: deleting %s built lines', (len(self.built_lines)))
+ del self.built_lines
+
+class TextWin(BaseTextWin):
+ def __init__(self, lines_nb_limit=None):
+ BaseTextWin.__init__(self, lines_nb_limit)
+
+ # the Lines of the highlights in that buffer
+ self.highlights = []
+ # the current HL position in that list NaN means that we’re not on
+ # an hl. -1 is a valid position (it's before the first hl of the
+ # list. i.e the separator, in the case where there’s no hl before
+ # it.)
+ self.hl_pos = float('nan')
+
+ # Keep track of the number of hl after the separator.
+ # This is useful to make “go to next highlight“ work after a “move to separator”.
+ self.nb_of_highlights_after_separator = 0
+
+ self.separator_after = None
+
def next_highlight(self):
"""
Go to the next highlight in the buffer.
@@ -130,22 +224,6 @@ class TextWin(Win):
if self.pos < 0 or self.pos >= len(self.built_lines):
self.pos = 0
- def scroll_up(self, dist=14):
- pos = self.pos
- self.pos += dist
- if self.pos + self.height > len(self.built_lines):
- self.pos = len(self.built_lines) - self.height
- if self.pos < 0:
- self.pos = 0
- return self.pos != pos
-
- def scroll_down(self, dist=14):
- pos = self.pos
- self.pos -= dist
- if self.pos <= 0:
- self.pos = 0
- return self.pos != pos
-
def scroll_to_separator(self):
"""
Scroll until separator is centered. If no separator is
@@ -343,12 +421,6 @@ class TextWin(Win):
self.width,
to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR))
- def write_text(self, y, x, txt):
- """
- write the text of a line.
- """
- self.addstr_colored(txt, y, x)
-
def write_ack(self):
color = get_theme().COLOR_CHAR_ACK
self._win.attron(to_curses_attr(color))
@@ -377,41 +449,6 @@ class TextWin(Win):
if highlight and hl_color == "reverse":
self._win.attroff(curses.A_REVERSE)
- def write_time(self, time):
- """
- Write the date on the yth line of the window
- """
- if time:
- self.addstr(time)
- self.addstr(' ')
-
- def resize(self, height, width, y, x, room=None):
- if hasattr(self, 'width'):
- old_width = self.width
- else:
- old_width = None
- self._resize(height, width, y, x)
- if room and self.width != old_width:
- self.rebuild_everything(room)
-
- # reposition the scrolling after resize
- # (see #2450)
- buf_size = len(self.built_lines)
- if buf_size - self.pos < self.height:
- self.pos = buf_size - self.height
- if self.pos < 0:
- self.pos = 0
-
- def rebuild_everything(self, room):
- self.built_lines = []
- with_timestamps = config.get('show_timestamps')
- for message in room.messages:
- self.build_new_message(message, clean=False, timestamp=with_timestamps)
- if self.separator_after is message:
- self.build_new_message(None)
- while len(self.built_lines) > self.lines_nb_limit:
- self.built_lines.pop(0)
-
def modify_message(self, old_id, message):
"""
Find a message, and replace it with a new one
@@ -435,3 +472,86 @@ class TextWin(Win):
log.debug('** TextWin: deleting %s built lines', (len(self.built_lines)))
del self.built_lines
+class XMLTextWin(BaseTextWin):
+ def __init__(self):
+ BaseTextWin.__init__(self)
+
+ def refresh(self):
+ log.debug('Refresh: %s', self.__class__.__name__)
+ theme = get_theme()
+ if self.height <= 0:
+ return
+ if self.pos == 0:
+ lines = self.built_lines[-self.height:]
+ else:
+ lines = self.built_lines[-self.height-self.pos:-self.pos]
+ self._win.move(0, 0)
+ self._win.erase()
+ for y, line in enumerate(lines):
+ if line:
+ msg = line.msg
+ if line.start_pos == 0:
+ if msg.nickname == theme.CHAR_XML_OUT:
+ color = theme.COLOR_XML_OUT
+ elif msg.nickname == theme.CHAR_XML_IN:
+ color = theme.COLOR_XML_IN
+ self.write_time(msg.str_time)
+ self.write_prefix(msg.nickname, color)
+ self.addstr(' ')
+ if y != self.height-1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ for y, line in enumerate(lines):
+ offset = 0
+ # Offset for the timestamp (if any) plus a space after it
+ offset += len(line.msg.str_time)
+ # space
+ offset += 1
+
+ # Offset for the prefix
+ offset += poopt.wcswidth(truncate_nick(line.msg.nickname))
+ # space
+ offset += 1
+
+ self.write_text(y, offset,
+ line.prepend+line.msg.txt[line.start_pos:line.end_pos])
+ if y != self.height-1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ self._refresh()
+
+ def build_message(self, message, timestamp=False):
+ txt = message.txt
+ ret = []
+ default_color = None
+ nick = truncate_nick(message.nickname)
+ offset = 0
+ if nick:
+ offset += poopt.wcswidth(nick) + 1 # + nick + ' ' length
+ if message.str_time:
+ offset += 1 + len(message.str_time)
+ if get_theme().CHAR_TIME_LEFT and message.str_time:
+ offset += 1
+ if get_theme().CHAR_TIME_RIGHT and message.str_time:
+ offset += 1
+ lines = poopt.cut_text(txt, self.width-offset-1)
+ prepend = default_color if default_color else ''
+ attrs = []
+ for line in lines:
+ saved = Line(msg=message, start_pos=line[0], end_pos=line[1], prepend=prepend)
+ attrs = parse_attrs(message.txt[line[0]:line[1]], attrs)
+ if attrs:
+ prepend = FORMAT_CHAR + FORMAT_CHAR.join(attrs)
+ else:
+ if default_color:
+ prepend = default_color
+ else:
+ prepend = ''
+ ret.append(saved)
+ return ret
+
+ def write_prefix(self, nickname, color):
+ self._win.attron(to_curses_attr(color))
+ self.addstr(truncate_nick(nickname))
+ self._win.attroff(to_curses_attr(color))
+