summaryrefslogtreecommitdiff
path: root/poezio/windows
diff options
context:
space:
mode:
Diffstat (limited to 'poezio/windows')
-rw-r--r--poezio/windows/__init__.py4
-rw-r--r--poezio/windows/base_wins.py60
-rw-r--r--poezio/windows/bookmark_forms.py96
-rw-r--r--poezio/windows/data_forms.py39
-rw-r--r--poezio/windows/funcs.py60
-rw-r--r--poezio/windows/image.py56
-rw-r--r--poezio/windows/info_bar.py103
-rw-r--r--poezio/windows/info_wins.py153
-rw-r--r--poezio/windows/input_placeholders.py2
-rw-r--r--poezio/windows/inputs.py50
-rw-r--r--poezio/windows/list.py24
-rw-r--r--poezio/windows/misc.py8
-rw-r--r--poezio/windows/muc.py22
-rw-r--r--poezio/windows/roster_win.py116
-rw-r--r--poezio/windows/text_win.py578
15 files changed, 587 insertions, 784 deletions
diff --git a/poezio/windows/__init__.py b/poezio/windows/__init__.py
index 8775b0a2..bbd6dc30 100644
--- a/poezio/windows/__init__.py
+++ b/poezio/windows/__init__.py
@@ -17,7 +17,7 @@ from poezio.windows.list import ListWin, ColumnHeaderWin
from poezio.windows.misc import VerticalSeparator
from poezio.windows.muc import UserList, Topic
from poezio.windows.roster_win import RosterWin, ContactInfoWin
-from poezio.windows.text_win import BaseTextWin, TextWin, XMLTextWin
+from poezio.windows.text_win import TextWin
from poezio.windows.image import ImageWin
__all__ = [
@@ -28,5 +28,5 @@ __all__ = [
'BookmarksInfoWin', 'ConfirmStatusWin', 'HelpText', 'Input',
'HistoryInput', 'MessageInput', 'CommandInput', 'ListWin',
'ColumnHeaderWin', 'VerticalSeparator', 'UserList', 'Topic', 'RosterWin',
- 'ContactInfoWin', 'TextWin', 'XMLTextWin', 'ImageWin', 'BaseTextWin'
+ 'ContactInfoWin', 'TextWin', 'ImageWin'
]
diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py
index 6dabd7b8..658e1533 100644
--- a/poezio/windows/base_wins.py
+++ b/poezio/windows/base_wins.py
@@ -7,40 +7,37 @@ the text window, the roster window, etc.
A Tab (see the poezio.tabs module) is composed of multiple Windows
"""
-TAB_WIN = None
-
-import logging
-log = logging.getLogger(__name__)
+from __future__ import annotations
import curses
+import logging
import string
-from typing import Optional, Tuple
+from contextlib import contextmanager
+from typing import Optional, Tuple, TYPE_CHECKING, cast
from poezio.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\x1A'
+from poezio.ui.consts import FORMAT_CHAR
+log = logging.getLogger(__name__)
-class DummyWin:
- def __getattribute__(self, name: str):
- if name != '__bool__':
- return lambda *args, **kwargs: (0, 0)
- else:
- return object.__getattribute__(self, name)
-
- def __bool__(self) -> bool:
- return False
+if TYPE_CHECKING:
+ from _curses import _CursesWindow # pylint: disable=E0611
class Win:
__slots__ = ('_win', 'height', 'width', 'y', 'x')
+ width: int
+ height: int
+ x: int
+ y: int
+
def __init__(self) -> None:
- self._win = None
+ if TAB_WIN is None:
+ raise ValueError
+ self._win: _CursesWindow = TAB_WIN
self.height, self.width = 0, 0
def _resize(self, height: int, width: int, y: int, x: int) -> None:
@@ -49,11 +46,11 @@ class Win:
return
self.height, self.width, self.x, self.y = height, width, x, y
try:
+ if TAB_WIN is None:
+ raise ValueError('TAB_WIN is None')
self._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: int, width: int, y: int, x: int) -> None:
"""
@@ -76,6 +73,24 @@ class Win:
# of the screen.
pass
+ @contextmanager
+ def colored_text(self, color: Optional[Tuple]=None, attr: Optional[int]=None):
+ """Context manager which sets up an attr/color when inside"""
+ if color is None and attr is None:
+ yield None
+ return
+ mode: int
+ if color is not None:
+ mode = to_curses_attr(color)
+ if attr is not None:
+ mode = mode | attr
+ else:
+ # attr cannot be none here
+ mode = cast(int, attr)
+ self._win.attron(mode)
+ yield None
+ self._win.attroff(mode)
+
def addstr(self, *args) -> None:
"""
Safe call to addstr
@@ -160,3 +175,6 @@ class Win:
self.addnstr(' ' * size, size, to_curses_attr(color))
else:
self.addnstr(' ' * size, size)
+
+
+TAB_WIN: Optional[_CursesWindow] = None
diff --git a/poezio/windows/bookmark_forms.py b/poezio/windows/bookmark_forms.py
index 2940ef04..a0e57cc7 100644
--- a/poezio/windows/bookmark_forms.py
+++ b/poezio/windows/bookmark_forms.py
@@ -4,22 +4,23 @@ Windows used inthe bookmarkstab
import curses
from typing import List, Tuple, Optional
-from poezio.windows import base_wins
+from slixmpp import JID, InvalidJID
+
from poezio.windows.base_wins import Win
from poezio.windows.inputs import Input
from poezio.windows.data_forms import FieldInput, FieldInputMixin
from poezio.theming import to_curses_attr, get_theme
-from poezio.common import safeJID
from poezio.bookmarks import Bookmark, BookmarkList
class BookmarkNameInput(FieldInput, Input):
- def __init__(self, field) -> None:
+ def __init__(self, field: Bookmark) -> None:
FieldInput.__init__(self, field)
Input.__init__(self)
self.text = field.name
- self.pos = len(self.text)
+ self.pos = 0
+ self.view_pos = 0
self.color = get_theme().COLOR_NORMAL_TEXT
def save(self) -> None:
@@ -30,17 +31,24 @@ class BookmarkNameInput(FieldInput, Input):
class BookmarkJIDInput(FieldInput, Input):
- def __init__(self, field) -> None:
+ def __init__(self, field: Bookmark) -> None:
FieldInput.__init__(self, field)
Input.__init__(self)
- jid = safeJID(field.jid)
+ try:
+ jid = JID(field.jid)
+ except InvalidJID:
+ jid = JID('')
jid.resource = field.nick or None
self.text = jid.full
- self.pos = len(self.text)
+ self.pos = 0
+ self.view_pos = 0
self.color = get_theme().COLOR_NORMAL_TEXT
def save(self) -> None:
- jid = safeJID(self.get_text())
+ try:
+ jid = JID(self.get_text())
+ except InvalidJID:
+ jid = JID('')
self._field.jid = jid.bare
self._field.nick = jid.resource
@@ -49,14 +57,14 @@ class BookmarkJIDInput(FieldInput, Input):
class BookmarkMethodInput(FieldInputMixin):
- def __init__(self, field) -> None:
+ def __init__(self, field: Bookmark) -> None:
FieldInput.__init__(self, field)
Win.__init__(self)
self.options = ('local', 'remote')
# val_pos is the position of the currently selected option
self.val_pos = self.options.index(field.method)
- def do_command(self, key: str) -> None:
+ def do_command(self, key: str, raw: bool = False) -> None:
if key == 'KEY_LEFT':
if self.val_pos > 0:
self.val_pos -= 1
@@ -89,7 +97,7 @@ class BookmarkMethodInput(FieldInputMixin):
class BookmarkPasswordInput(FieldInput, Input):
- def __init__(self, field) -> None:
+ def __init__(self, field: Bookmark) -> None:
FieldInput.__init__(self, field)
Input.__init__(self)
self.text = field.password or ''
@@ -119,13 +127,13 @@ class BookmarkPasswordInput(FieldInput, Input):
class BookmarkAutojoinWin(FieldInputMixin):
- def __init__(self, field) -> None:
+ def __init__(self, field: Bookmark) -> None:
FieldInput.__init__(self, field)
Win.__init__(self)
self.last_key = 'KEY_RIGHT'
self.value = field.autojoin
- def do_command(self, key: str) -> None:
+ def do_command(self, key: str, raw: bool = False) -> None:
if key == 'KEY_LEFT' or key == 'KEY_RIGHT':
self.value = not self.value
self.last_key = key
@@ -155,14 +163,14 @@ class BookmarksWin(Win):
__slots__ = ('scroll_pos', '_current_input', 'current_horizontal_input',
'_bookmarks', 'lines')
- def __init__(self, bookmarks: BookmarkList, height: int, width: int, y: int, x: int) -> None:
- self._win = base_wins.TAB_WIN.derwin(height, width, y, x)
+ def __init__(self, bookmarks: BookmarkList) -> None:
+ Win.__init__(self)
self.scroll_pos = 0
self._current_input = 0
self.current_horizontal_input = 0
self._bookmarks = list(bookmarks)
- self.lines = [] # type: List[Tuple[BookmarkNameInput, BookmarkJIDInput, BookmarkPasswordInput, BookmarkAutojoinWin, BookmarkMethodInput]]
- for bookmark in sorted(self._bookmarks, key=lambda x: x.jid):
+ self.lines: List[Tuple[BookmarkNameInput, BookmarkJIDInput, BookmarkPasswordInput, BookmarkAutojoinWin, BookmarkMethodInput]] = []
+ for bookmark in sorted(self._bookmarks, key=lambda x: str(x.jid)):
self.lines.append((BookmarkNameInput(bookmark),
BookmarkJIDInput(bookmark),
BookmarkPasswordInput(bookmark),
@@ -190,11 +198,13 @@ class BookmarksWin(Win):
BookmarkPasswordInput(bookmark),
BookmarkAutojoinWin(bookmark),
BookmarkMethodInput(bookmark)))
- self.lines[self.current_input][
- self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ if len(self.lines) > 1:
+ self.lines[self.current_input][
+ self.current_horizontal_input].set_color(
+ get_theme().COLOR_NORMAL_TEXT)
self.current_horizontal_input = 0
- self.current_input = len(self.lines) - 1
+ if len(self.lines) > 1:
+ self.current_input = len(self.lines) - 1
if self.current_input - self.scroll_pos > self.height - 1:
self.scroll_pos = self.current_input - self.height + 1
self.refresh()
@@ -212,9 +222,7 @@ class BookmarksWin(Win):
return bm
def resize(self, height: int, width: int, y: int, x: int) -> None:
- self.height = height
- self.width = width
- self._win = base_wins.TAB_WIN.derwin(height, width, y, x)
+ super().resize(height, width, y, x)
# Adjust the scroll position, if resizing made the window too small
# for the cursor to be visible
while self.current_input - self.scroll_pos > self.height - 1:
@@ -245,9 +253,10 @@ class BookmarksWin(Win):
return
if self.current_input == 0:
return
+ theme = get_theme()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.current_input -= 1
# Adjust the scroll position if the current_input would be outside
# of the visible area
@@ -256,20 +265,21 @@ class BookmarksWin(Win):
self.refresh()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
def go_to_next_horizontal_input(self) -> None:
if not self.lines:
return
+ theme = get_theme()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.current_horizontal_input += 1
- if self.current_horizontal_input > 3:
+ if self.current_horizontal_input > 4:
self.current_horizontal_input = 0
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
def go_to_next_page(self) -> bool:
if not self.lines:
@@ -278,9 +288,10 @@ class BookmarksWin(Win):
if self.current_input == len(self.lines) - 1:
return False
+ theme = get_theme()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
inc = min(self.height, len(self.lines) - self.current_input - 1)
if self.current_input + inc - self.scroll_pos > self.height - 1:
@@ -291,7 +302,7 @@ class BookmarksWin(Win):
self.current_input += inc
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
return True
def go_to_previous_page(self) -> bool:
@@ -301,9 +312,10 @@ class BookmarksWin(Win):
if self.current_input == 0:
return False
+ theme = get_theme()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
dec = min(self.height, self.current_input)
self.current_input -= dec
@@ -314,7 +326,7 @@ class BookmarksWin(Win):
self.refresh()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
return True
def go_to_previous_horizontal_input(self) -> None:
@@ -322,19 +334,20 @@ class BookmarksWin(Win):
return
if self.current_horizontal_input == 0:
return
+ theme = get_theme()
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.current_horizontal_input -= 1
self.lines[self.current_input][
self.current_horizontal_input].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
- def on_input(self, key: str) -> None:
+ def on_input(self, key: str, raw: bool = False) -> None:
if not self.lines:
return
self.lines[self.current_input][
- self.current_horizontal_input].do_command(key)
+ self.current_horizontal_input].do_command(key, raw=raw)
def refresh(self) -> None:
# store the cursor status
@@ -356,7 +369,7 @@ class BookmarksWin(Win):
continue
if i >= self.height + self.scroll_pos:
break
- for j in range(4):
+ for j in range(5):
inp[j].refresh()
if self.lines and self.current_input < self.height - 1:
@@ -377,5 +390,8 @@ class BookmarksWin(Win):
def save(self) -> None:
for line in self.lines:
- for item in line:
- item.save()
+ line[0].save()
+ line[1].save()
+ line[2].save()
+ line[3].save()
+ line[4].save()
diff --git a/poezio/windows/data_forms.py b/poezio/windows/data_forms.py
index b8dd8531..db174703 100644
--- a/poezio/windows/data_forms.py
+++ b/poezio/windows/data_forms.py
@@ -6,6 +6,7 @@ does not inherit from the Win base class), as it will create the
others when needed.
"""
+from typing import Type
from poezio.windows import base_wins
from poezio.windows.base_wins import Win
from poezio.windows.inputs import Input
@@ -189,7 +190,7 @@ class TextMultiWin(FieldInputMixin):
if not self.options or self.options[-1] != '':
self.options.append('')
else:
- self.edition_input.do_command(key)
+ self.edition_input.do_command(key, raw=raw)
self.refresh()
def refresh(self):
@@ -272,7 +273,7 @@ class ListMultiWin(FieldInputMixin):
self._field.set_answer(values)
def get_help_message(self):
- return '←, →: Switch between the value. Space: select or unselect a value'
+ return '←, →: Switch between the value. Space: select or deselect a value'
class ListSingleWin(FieldInputMixin):
@@ -330,7 +331,8 @@ class TextSingleWin(FieldInputMixin, Input):
Input.__init__(self)
self.text = field.get_value() if isinstance(field.get_value(), str)\
else ""
- self.pos = len(self.text)
+ self.pos = 0
+ self.view_pos = 0
self.color = get_theme().COLOR_NORMAL_TEXT
def reply(self):
@@ -396,10 +398,10 @@ class FormWin:
for (name, field) in self._form.getFields().items():
if field['type'] == 'hidden':
continue
- try:
+ if field['type'] not in self.input_classes:
+ input_class: Type[FieldInputMixin] = TextSingleWin
+ else:
input_class = self.input_classes[field['type']]
- except IndexError:
- continue
label = field['label']
desc = field['desc']
if field['type'] == 'fixed':
@@ -438,10 +440,11 @@ class FormWin:
return
if self.current_input == len(self.inputs) - 1:
return
+ theme = get_theme()
self.inputs[self.current_input]['input'].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.inputs[self.current_input]['label'].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.current_input += 1
jump = 0
while self.current_input + jump != len(
@@ -460,19 +463,20 @@ class FormWin:
self.scroll_pos += 1
self.refresh()
self.inputs[self.current_input]['input'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
self.inputs[self.current_input]['label'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
def go_to_previous_input(self):
if not self.inputs:
return
if self.current_input == 0:
return
+ theme = get_theme()
self.inputs[self.current_input]['input'].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.inputs[self.current_input]['label'].set_color(
- get_theme().COLOR_NORMAL_TEXT)
+ theme.COLOR_NORMAL_TEXT)
self.current_input -= 1
jump = 0
while self.current_input - jump > 0 and self.inputs[self.current_input
@@ -489,9 +493,9 @@ class FormWin:
self.refresh()
self.current_input -= jump
self.inputs[self.current_input]['input'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
self.inputs[self.current_input]['label'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ theme.COLOR_SELECTED_ROW)
def on_input(self, key, raw=False):
if not self.inputs:
@@ -521,11 +525,10 @@ class FormWin:
inp['input'].refresh()
inp['label'].refresh()
if self.inputs and self.current_input < self.height - 1:
- self.inputs[self.current_input]['input'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ color = get_theme().COLOR_SELECTED_ROW
+ self.inputs[self.current_input]['input'].set_color(color)
self.inputs[self.current_input]['input'].refresh()
- self.inputs[self.current_input]['label'].set_color(
- get_theme().COLOR_SELECTED_ROW)
+ self.inputs[self.current_input]['label'].set_color(color)
self.inputs[self.current_input]['label'].refresh()
def refresh_current_input(self):
diff --git a/poezio/windows/funcs.py b/poezio/windows/funcs.py
deleted file mode 100644
index 22977374..00000000
--- a/poezio/windows/funcs.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""
-Standalone functions used by the modules
-"""
-
-import string
-from typing import Optional, List
-from poezio.windows.base_wins import FORMAT_CHAR, format_chars
-
-DIGITS = string.digits + '-'
-
-
-def find_first_format_char(text: str,
- chars: str = None) -> int:
- to_find = chars or format_chars
- pos = -1
- for char in to_find:
- p = text.find(char)
- if p == -1:
- continue
- if pos == -1 or p < pos:
- pos = p
- return pos
-
-
-def truncate_nick(nick: Optional[str], size=10) -> Optional[str]:
- if size < 1:
- size = 1
- if nick and len(nick) > size:
- return nick[:size] + '…'
- return nick
-
-
-def parse_attrs(text: str, previous: Optional[List[str]] = None) -> List[str]:
- 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 = '\0'
- if attr_char == 'o':
- attrs = []
- elif attr_char == 'u':
- attrs.append('u')
- elif attr_char == 'b':
- attrs.append('b')
- elif attr_char == 'i':
- attrs.append('i')
- if attr_char in 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
diff --git a/poezio/windows/image.py b/poezio/windows/image.py
index 75f4d588..2862d2d9 100644
--- a/poezio/windows/image.py
+++ b/poezio/windows/image.py
@@ -2,6 +2,8 @@
Defines a window which contains either an image or a border.
"""
+from __future__ import annotations
+
import curses
from io import BytesIO
@@ -11,6 +13,15 @@ try:
except ImportError:
HAS_PIL = False
+try:
+ import gi
+ gi.require_version('Rsvg', '2.0')
+ from gi.repository import Rsvg
+ import cairo
+ HAS_RSVG = True
+except (ImportError, ValueError, AttributeError):
+ HAS_RSVG = False
+
from poezio.windows.base_wins import Win
from poezio.theming import get_theme, to_curses_attr
from poezio.xhtml import _parse_css_color
@@ -19,6 +30,36 @@ from poezio.config import config
from typing import Tuple, Optional, Callable
+MAX_SIZE = 16
+
+
+def render_svg(svg: bytes) -> Optional[Image.Image]:
+ if not HAS_RSVG:
+ return None
+ try:
+ handle = Rsvg.Handle.new_from_data(svg)
+ dimensions = handle.get_dimensions()
+ biggest_dimension = max(dimensions.width, dimensions.height)
+ scale = MAX_SIZE / biggest_dimension
+ translate_x = (biggest_dimension - dimensions.width) / 2
+ translate_y = (biggest_dimension - dimensions.height) / 2
+
+ surface = cairo.ImageSurface(cairo.Format.ARGB32, MAX_SIZE, MAX_SIZE)
+ context = cairo.Context(surface)
+ context.scale(scale, scale)
+ context.translate(translate_x, translate_y)
+ handle.render_cairo(context)
+ data = surface.get_data()
+ image = Image.frombytes('RGBA', (MAX_SIZE, MAX_SIZE), data.tobytes())
+ # This is required because Cairo uses a BGRA (in host endianness)
+ # format, and PIL an ABGR (in byte order) format. Yes, this is
+ # confusing.
+ b, g, r, a = image.split()
+ return Image.merge('RGB', (r, g, b))
+ except Exception:
+ return None
+
+
class ImageWin(Win):
"""
A window which contains either an image or a border.
@@ -27,10 +68,10 @@ class ImageWin(Win):
__slots__ = ('_image', '_display_avatar')
def __init__(self) -> None:
- self._image = None # type: Optional[Image]
+ self._image: Optional[Image.Image] = None
Win.__init__(self)
- if config.get('image_use_half_blocks'):
- self._display_avatar = self._display_avatar_half_blocks # type: Callable[[int, int], None]
+ if config.getbool('image_use_half_blocks'):
+ self._display_avatar: Callable[[int, int], None] = self._display_avatar_half_blocks
else:
self._display_avatar = self._display_avatar_full_blocks
@@ -45,7 +86,14 @@ class ImageWin(Win):
if data is not None and HAS_PIL:
image_file = BytesIO(data)
try:
- image = Image.open(image_file)
+ try:
+ image = Image.open(image_file)
+ except OSError:
+ # TODO: Make the caller pass the MIME type, so we don’t
+ # have to try all renderers like that.
+ image = render_svg(data)
+ if image is None:
+ raise
except OSError:
self._display_border()
else:
diff --git a/poezio/windows/info_bar.py b/poezio/windows/info_bar.py
index 15821c10..6e6c3bbd 100644
--- a/poezio/windows/info_bar.py
+++ b/poezio/windows/info_bar.py
@@ -5,14 +5,19 @@ This window is the one listing the current opened tabs in poezio.
The GlobalInfoBar can be either horizontal or vertical
(VerticalGlobalInfoBar).
"""
+import curses
+import itertools
import logging
-log = logging.getLogger(__name__)
-import curses
+from typing import List, Optional
from poezio.config import config
from poezio.windows.base_wins import Win
from poezio.theming import get_theme, to_curses_attr
+from poezio.common import unique_prefix_of
+from poezio.colors import ccg_text_to_color
+
+log = logging.getLogger(__name__)
class GlobalInfoBar(Win):
@@ -25,42 +30,93 @@ class GlobalInfoBar(Win):
def refresh(self) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
+ theme = get_theme()
self.addstr(0, 0, "[",
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
- show_names = config.get('show_tab_names')
- show_nums = config.get('show_tab_numbers')
- use_nicks = config.get('use_tab_nicks')
- show_inactive = config.get('show_inactive_tabs')
+ show_names = config.getbool('show_tab_names')
+ show_nums = config.getbool('show_tab_numbers')
+ use_nicks = config.getbool('use_tab_nicks')
+ show_inactive = config.getbool('show_inactive_tabs')
+ unique_prefix_tab_names = config.getbool('unique_prefix_tab_names')
+ autocolor_tab_names = config.getbool('autocolor_tab_names')
+
+ if unique_prefix_tab_names:
+ unique_prefixes: List[Optional[str]] = [None] * len(self.core.tabs)
+ sorted_tab_indices = sorted(
+ (str(tab.name), i)
+ for i, tab in enumerate(self.core.tabs)
+ )
+ prev_name = ""
+ for (name, i), next_item in itertools.zip_longest(
+ sorted_tab_indices, sorted_tab_indices[1:]):
+ # TODO: should this maybe use something smarter than .lower()?
+ # something something stringprep?
+ name = name.lower()
+ prefix_prev = unique_prefix_of(name, prev_name)
+ if next_item is not None:
+ prefix_next = unique_prefix_of(name, next_item[0].lower())
+ else:
+ prefix_next = name[0]
+
+ # to be unique, we have to use the longest prefix
+ if len(prefix_next) > len(prefix_prev):
+ prefix = prefix_next
+ else:
+ prefix = prefix_prev
+
+ unique_prefixes[i] = prefix
+ prev_name = name
for nb, tab in enumerate(self.core.tabs):
if not tab:
continue
color = tab.color
- if not show_inactive and color is get_theme().COLOR_TAB_NORMAL:
+ if not show_inactive and color is theme.COLOR_TAB_NORMAL and (
+ tab.priority < 0):
continue
+ if autocolor_tab_names:
+ # TODO: in case of private MUC conversations, we should try to
+ # get hold of more information to make the colour the same as
+ # the nickname colour in the MUC.
+ fgcolor, bgcolor, *flags = color
+ # this is fugly, but I’m not sure how to improve it... since
+ # apparently the state is only kept in the color -.-
+ if (color == theme.COLOR_TAB_HIGHLIGHT or
+ color == theme.COLOR_TAB_PRIVATE):
+ fgcolor = ccg_text_to_color(theme.ccg_palette, tab.name)
+ bgcolor = -1
+ flags = theme.MODE_TAB_IMPORTANT
+ elif color == theme.COLOR_TAB_NEW_MESSAGE:
+ fgcolor = ccg_text_to_color(theme.ccg_palette, tab.name)
+ bgcolor = -1
+ flags = theme.MODE_TAB_NORMAL
+
+ color = (fgcolor, bgcolor) + tuple(flags)
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:
+ if unique_prefix_tab_names:
+ self.addstr(unique_prefixes[nb], to_curses_attr(color))
+ elif 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))
+ to_curses_attr(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))
+ to_curses_attr(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))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
self._refresh()
@@ -76,17 +132,24 @@ class VerticalGlobalInfoBar(Win):
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'):
+ theme = get_theme()
+ if not config.getbool('show_inactive_tabs'):
sorted_tabs = [
tab for tab in sorted_tabs
- if tab.vertical_color != get_theme().COLOR_VERTICAL_TAB_NORMAL
+ if (
+ tab.vertical_color != theme.COLOR_VERTICAL_TAB_NORMAL or
+ tab.priority > 0
+ )
]
nb_tabs = len(sorted_tabs)
- use_nicks = config.get('use_tab_nicks')
+ use_nicks = config.getbool('use_tab_nicks')
if nb_tabs >= height:
+ # TODO: As sorted_tabs filters out gap tabs this ensures pos is
+ # always set, preventing UnboundLocalError. Now is this how this
+ # should be fixed.
+ pos = 0
for y, tab in enumerate(sorted_tabs):
- if tab.vertical_color == get_theme(
- ).COLOR_VERTICAL_TAB_CURRENT:
+ if tab.vertical_color == theme.COLOR_VERTICAL_TAB_CURRENT:
pos = y
break
# center the current tab as much as possible
@@ -96,20 +159,20 @@ class VerticalGlobalInfoBar(Win):
sorted_tabs = sorted_tabs[-height:]
else:
sorted_tabs = sorted_tabs[pos - height // 2:pos + height // 2]
- asc_sort = (config.get('vertical_tab_list_sort') == 'asc')
+ asc_sort = (config.getstr('vertical_tab_list_sort') == 'asc')
for y, tab in enumerate(sorted_tabs):
color = tab.vertical_color
if asc_sort:
y = height - y - 1
self.addstr(y, 0, "%2d" % tab.nb,
- to_curses_attr(get_theme().COLOR_VERTICAL_TAB_NUMBER))
+ to_curses_attr(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)
+ separator = to_curses_attr(theme.COLOR_VERTICAL_SEPARATOR)
self._win.attron(separator)
self._win.vline(0, width - 1, curses.ACS_VLINE, height)
self._win.attroff(separator)
diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py
index abc0a401..227dc115 100644
--- a/poezio/windows/info_wins.py
+++ b/poezio/windows/info_wins.py
@@ -3,15 +3,27 @@ Module defining all the "info wins", ie the bar which is on top of the
info buffer in normal tabs
"""
+from __future__ import annotations
+
+from typing import Optional, Dict, TYPE_CHECKING, Any
+
import logging
-log = logging.getLogger(__name__)
-from poezio.common import safeJID
+from slixmpp import JID, InvalidJID
+
from poezio.config import config
from poezio.windows.base_wins import Win
-from poezio.windows.funcs import truncate_nick
+from poezio.ui.funcs import truncate_nick
from poezio.theming import get_theme, to_curses_attr
+from poezio.colors import ccg_text_to_color
+
+if TYPE_CHECKING:
+ from poezio.user import User
+ from poezio.tabs import MucTab
+ from poezio.windows import TextWin
+
+log = logging.getLogger(__name__)
class InfoWin(Win):
@@ -92,11 +104,18 @@ class PrivateInfoWin(InfoWin):
to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
def write_room_name(self, name):
- jid = safeJID(name)
+ # TODO: autocolour this too, but we need more info about the occupant
+ # (whether we know its real jid) and the room (whether it is
+ # anonymous) to provide correct colouring.
+ try:
+ jid = JID(name)
+ except InvalidJID:
+ jid = JID('')
room_name, nick = jid.bare, jid.resource
- self.addstr(nick, to_curses_attr(get_theme().COLOR_PRIVATE_NAME))
+ theme = get_theme()
+ self.addstr(nick, to_curses_attr(theme.COLOR_PRIVATE_NAME))
txt = ' from room %s' % room_name
- self.addstr(txt, to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr(txt, to_curses_attr(theme.COLOR_INFORMATION_BAR))
def write_chatstate(self, state):
if state:
@@ -119,15 +138,16 @@ class MucListInfoWin(InfoWin):
def refresh(self, name=None, window=None):
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
+ theme = get_theme()
if name:
self.addstr(name,
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
else:
self.addstr(self.message,
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
if window:
self.print_scroll_position(window)
- self.finish_line(get_theme().COLOR_INFORMATION_BAR)
+ self.finish_line(theme.COLOR_INFORMATION_BAR)
self._refresh()
@@ -147,7 +167,10 @@ class ConversationInfoWin(InfoWin):
# 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)
+ try:
+ jid = JID(jid)
+ except InvalidJID:
+ jid = JID('')
if contact:
if jid.resource:
resource = contact[jid.full]
@@ -161,7 +184,7 @@ class ConversationInfoWin(InfoWin):
# resource can now be a Resource: user is in the roster and online
# or resource is None: user is in the roster but offline
self._win.erase()
- if config.get('show_jid_in_conversations'):
+ if config.getbool('show_jid_in_conversations'):
self.write_contact_jid(jid)
self.write_contact_information(contact)
self.write_resource_information(resource)
@@ -176,9 +199,9 @@ class ConversationInfoWin(InfoWin):
Write all information added by plugins by getting the
value returned by the callbacks.
"""
+ color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR)
for plugin in information.values():
- self.addstr(plugin(jid),
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr(plugin(jid), color)
def write_resource_information(self, resource):
"""
@@ -188,38 +211,58 @@ class ConversationInfoWin(InfoWin):
presence = "unavailable"
else:
presence = resource.presence
- color = get_theme().color_show(presence)
+ theme = get_theme()
+ color = theme.color_show(presence)
if not presence:
- presence = get_theme().CHAR_STATUS
- self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ presence = theme.CHAR_STATUS
+ self.addstr('[', to_curses_attr(theme.COLOR_INFORMATION_BAR))
self.addstr(presence, to_curses_attr(color))
if resource and resource.status:
shortened = resource.status[:20] + (resource.status[:20] and '…')
self.addstr(' %s' % shortened,
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
- self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
+ self.addstr(']', to_curses_attr(theme.COLOR_INFORMATION_BAR))
def write_contact_information(self, contact):
"""
Write the information about the contact
"""
+ theme = get_theme()
+ color = to_curses_attr(theme.COLOR_INFORMATION_BAR)
+ if config.get('autocolor_tab_names') and contact is not None:
+ name_color = (
+ ccg_text_to_color(theme.ccg_palette, str(contact.bare_jid)),
+ -1,
+ theme.MODE_TAB_NAME,
+ )
+ else:
+ name_color = color
+
if not contact:
- self.addstr("(contact not in roster)",
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr("(contact not in roster)", color)
return
display_name = contact.name
if display_name:
- self.addstr('%s ' % (display_name),
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr('%s ' % (display_name), name_color)
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))
+ theme = get_theme()
+ color = to_curses_attr(theme.COLOR_INFORMATION_BAR)
+ if config.get('autocolor_tab_names'):
+ name_color = (
+ ccg_text_to_color(theme.ccg_palette, str(contact.jid)),
+ -1,
+ theme.MODE_TAB_NAME,
+ )
+ else:
+ name_color = theme.COLOR_CONVERSATION_NAME
+
+ self.addstr('[', color)
+ self.addstr(jid.full, to_curses_attr(name_color))
+ self.addstr('] ', color)
def write_chatstate(self, state):
if state:
@@ -236,14 +279,16 @@ class DynamicConversationInfoWin(ConversationInfoWin):
"""
log.debug("write_contact_jid DynamicConversationInfoWin, jid: %s",
jid.resource)
- self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ theme = get_theme()
+ color = to_curses_attr(theme.COLOR_INFORMATION_BAR)
+ self.addstr('[', color)
self.addstr(jid.bare,
- to_curses_attr(get_theme().COLOR_CONVERSATION_NAME))
+ to_curses_attr(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))
+ to_curses_attr(theme.COLOR_CONVERSATION_RESOURCE))
+ self.addstr('] ', color)
class MucInfoWin(InfoWin):
@@ -254,10 +299,16 @@ class MucInfoWin(InfoWin):
__slots__ = ()
- def __init__(self):
+ def __init__(self) -> None:
InfoWin.__init__(self)
- def refresh(self, room, window=None, user=None, information=None):
+ def refresh(
+ self,
+ room: MucTab,
+ window: Optional[TextWin] = None,
+ user: Optional[User] = None,
+ information: Optional[Dict[str, Any]] = None
+ ) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
self.write_room_name(room)
@@ -277,22 +328,34 @@ class MucInfoWin(InfoWin):
Write all information added by plugins by getting the
value returned by the callbacks.
"""
+ color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR)
for plugin in information.values():
- self.addstr(plugin(jid),
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr(plugin(jid), color)
def write_room_name(self, room):
- self.addstr('[', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ theme = get_theme()
+ color = to_curses_attr(theme.COLOR_INFORMATION_BAR)
+ label_color = theme.COLOR_GROUPCHAT_NAME
+
+ if config.get('autocolor_tab_names'):
+ label_color = ccg_text_to_color(
+ theme.ccg_palette,
+ room.jid.bare,
+ ), -1, theme.MODE_TAB_NAME
+
+ self.addstr('[', color)
self.addstr(room.name,
- to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
- self.addstr(']', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(label_color))
+ self.addstr(']', color)
def write_participants_number(self, room):
- self.addstr('{', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ theme = get_theme()
+ color = to_curses_attr(theme.COLOR_INFORMATION_BAR)
+ self.addstr('{', color)
self.addstr(
str(len(room.users)),
- to_curses_attr(get_theme().COLOR_GROUPCHAT_NAME))
- self.addstr('} ', to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ to_curses_attr(theme.COLOR_GROUPCHAT_NAME))
+ self.addstr('} ', color)
def write_disconnected(self, room):
"""
@@ -338,7 +401,10 @@ class ConversationStatusMessageWin(InfoWin):
def refresh(self, jid, contact):
log.debug('Refresh: %s', self.__class__.__name__)
- jid = safeJID(jid)
+ try:
+ jid = JID(jid)
+ except InvalidJID:
+ jid = JID('')
if contact:
if jid.resource:
resource = contact[jid.full]
@@ -386,10 +452,11 @@ class ConfirmStatusWin(Win):
def refresh(self):
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
+ theme = get_theme()
if self.critical:
- color = get_theme().COLOR_WARNING_PROMPT
+ color = theme.COLOR_WARNING_PROMPT
else:
- color = get_theme().COLOR_INFORMATION_BAR
+ color = theme.COLOR_INFORMATION_BAR
c_color = to_curses_attr(color)
self.addstr(self.text, c_color)
self.finish_line(color)
diff --git a/poezio/windows/input_placeholders.py b/poezio/windows/input_placeholders.py
index 4d414636..3ec57583 100644
--- a/poezio/windows/input_placeholders.py
+++ b/poezio/windows/input_placeholders.py
@@ -23,7 +23,7 @@ class HelpText(Win):
def __init__(self, text: str = '') -> None:
Win.__init__(self)
- self.txt = text # type: str
+ self.txt: str = text
def refresh(self, txt: Optional[str] = None) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py
index c0c73419..01b94ac0 100644
--- a/poezio/windows/inputs.py
+++ b/poezio/windows/inputs.py
@@ -5,13 +5,14 @@ Text inputs.
import curses
import logging
import string
-from typing import List, Dict, Callable, Optional
+from typing import List, Dict, Callable, Optional, ClassVar
from poezio import keyboard
from poezio import common
from poezio import poopt
-from poezio.windows.base_wins import Win, format_chars
-from poezio.windows.funcs import find_first_format_char
+from poezio.windows.base_wins import Win
+from poezio.ui.consts import FORMAT_CHARS
+from poezio.ui.funcs import find_first_format_char
from poezio.config import config
from poezio.theming import to_curses_attr
@@ -40,7 +41,7 @@ class Input(Win):
# it easy cut and paste text between various input
def __init__(self) -> None:
- self.key_func = {
+ self.key_func: Dict[str, Callable] = {
"KEY_LEFT": self.key_left,
"KEY_RIGHT": self.key_right,
"KEY_END": self.key_end,
@@ -65,7 +66,7 @@ class Input(Win):
'^?': self.key_backspace,
"M-^?": self.delete_word,
# '^J': self.add_line_break,
- } # type: Dict[str, Callable]
+ }
Win.__init__(self)
self.text = ''
self.pos = 0 # The position of the “cursor” in the text
@@ -75,8 +76,8 @@ class Input(Win):
# screen
self.on_input = DEFAULT_ON_INPUT # callback called on any key pressed
self.color = None # use this color on addstr
- self.last_completion = None # type: Optional[str]
- self.hit_list = [] # type: List[str]
+ self.last_completion: Optional[str] = None
+ self.hit_list: List[str] = []
def on_delete(self) -> None:
"""
@@ -109,7 +110,7 @@ class Input(Win):
"""
if self.pos == 0:
return True
- separators = string.punctuation + ' '
+ separators = string.punctuation + ' ' + '\n'
while self.pos > 0 and self.text[self.pos - 1] in separators:
self.key_left()
while self.pos > 0 and self.text[self.pos - 1] not in separators:
@@ -122,7 +123,7 @@ class Input(Win):
"""
if self.is_cursor_at_end():
return True
- separators = string.punctuation + ' '
+ separators = string.punctuation + ' ' + '\n'
while not self.is_cursor_at_end() and self.text[self.pos] in separators:
self.key_right()
while not self.is_cursor_at_end() and self.text[self.
@@ -134,7 +135,7 @@ class Input(Win):
"""
Delete the word just before the cursor
"""
- separators = string.punctuation + ' '
+ separators = string.punctuation + ' ' + '\n'
while self.pos > 0 and self.text[self.pos - 1] in separators:
self.key_backspace()
while self.pos > 0 and self.text[self.pos - 1] not in separators:
@@ -145,7 +146,7 @@ class Input(Win):
"""
Delete the word just after the cursor
"""
- separators = string.punctuation + ' '
+ separators = string.punctuation + ' ' + '\n'
while not self.is_cursor_at_end() and self.text[self.pos] in separators:
self.key_dc()
while not self.is_cursor_at_end() and self.text[self.
@@ -408,12 +409,14 @@ class Input(Win):
Normal completion
"""
pos = self.pos
- if pos < len(
- self.text) and after.endswith(' ') and self.text[pos] == ' ':
+ if pos < len(self.text) and after.endswith(' ') and self.text[pos] in ' \n':
after = after[:
-1] # remove the last space if we are already on a space
if not self.last_completion:
space_before_cursor = self.text.rfind(' ', 0, pos)
+ line_before_cursor = self.text.rfind('\n', 0, pos)
+ if line_before_cursor > space_before_cursor:
+ space_before_cursor = line_before_cursor
if space_before_cursor != -1:
begin = self.text[space_before_cursor + 1:pos]
else:
@@ -487,7 +490,7 @@ class Input(Win):
(\x0E to \x19 instead of \x19 + attr). We do not use any }
char in this version
"""
- chars = format_chars + '\n'
+ chars = FORMAT_CHARS + '\n'
if y is not None and x is not None:
self.move(y, x)
format_char = find_first_format_char(text, chars)
@@ -497,7 +500,7 @@ class Input(Win):
if text[format_char] == '\n':
attr_char = '|'
else:
- attr_char = self.text_attributes[format_chars.index(
+ attr_char = self.text_attributes[FORMAT_CHARS.index(
text[format_char])]
self.addstr(text[:format_char])
self.addstr(attr_char, curses.A_REVERSE)
@@ -589,9 +592,10 @@ class HistoryInput(Input):
An input with colors and stuff, plus an history
^R allows to search inside the history (as in a shell)
"""
- __slots__ = ('help_message', 'histo_pos', 'current_completed', 'search')
+ __slots__ = ('help_message', 'histo_pos', 'current_completed', 'search',
+ 'history')
- history = [] # type: List[str]
+ global_history: ClassVar[List[str]] = []
def __init__(self) -> None:
Input.__init__(self)
@@ -600,8 +604,10 @@ class HistoryInput(Input):
self.current_completed = ''
self.key_func['^R'] = self.toggle_search
self.search = False
- if config.get('separate_history'):
- self.history = [] # type: List[str]
+ if config.getbool('separate_history'):
+ self.history: List[str] = []
+ else:
+ self.history = self.__class__.global_history
def toggle_search(self) -> None:
if self.help_message:
@@ -678,7 +684,7 @@ class MessageInput(HistoryInput):
Also letting the user enter colors or other text markups
"""
# The history is common to all MessageInput
- history = [] # type: List[str]
+ global_history: ClassVar[List[str]] = []
def __init__(self) -> None:
HistoryInput.__init__(self)
@@ -695,7 +701,7 @@ class MessageInput(HistoryInput):
def cb(attr_char):
if attr_char in self.text_attributes:
- char = format_chars[self.text_attributes.index(attr_char)]
+ char = FORMAT_CHARS[self.text_attributes.index(attr_char)]
self.do_command(char, False)
self.rewrite_text()
@@ -724,7 +730,7 @@ class CommandInput(HistoryInput):
HelpMessage when a command is started
The on_input callback
"""
- history = [] # type: List[str]
+ global_history: ClassVar[List[str]] = []
def __init__(self, help_message: str, on_abort, on_success, on_input=None) -> None:
HistoryInput.__init__(self)
diff --git a/poezio/windows/list.py b/poezio/windows/list.py
index f03dcf6a..1c5d834f 100644
--- a/poezio/windows/list.py
+++ b/poezio/windows/list.py
@@ -24,10 +24,10 @@ class ListWin(Win):
def __init__(self, columns: Dict[str, int], with_headers: bool = True) -> None:
Win.__init__(self)
- self._columns = columns # type: Dict[str, int]
- self._columns_sizes = {} # type: Dict[str, int]
+ self._columns: Dict[str, int] = columns
+ self._columns_sizes: Dict[str, int] = {}
self.sorted_by = (None, None) # for example: ('name', '↑')
- self.lines = [] # type: List[str]
+ self.lines: List[str] = []
self._selected_row = 0
self._starting_pos = 0 # The column number from which we start the refresh
@@ -40,7 +40,7 @@ class ListWin(Win):
def empty(self) -> None:
"""
- emtpy the list and reset some important values as well
+ empty the list and reset some important values as well
"""
self.lines = []
self._selected_row = 0
@@ -94,6 +94,7 @@ class ListWin(Win):
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
lines = self.lines[self._starting_pos:self._starting_pos + self.height]
+ color = to_curses_attr(get_theme().COLOR_INFORMATION_BAR)
for y, line in enumerate(lines):
x = 0
for col in self._columns.items():
@@ -106,9 +107,7 @@ class ListWin(Win):
if not txt:
continue
if line is self.lines[self._selected_row]:
- self.addstr(
- y, x, txt[:size],
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
+ self.addstr(y, x, txt[:size], color)
else:
self.addstr(y, x, txt[:size])
x += size
@@ -174,7 +173,7 @@ class ColumnHeaderWin(Win):
def __init__(self, columns: List[str]) -> None:
Win.__init__(self)
self._columns = columns
- self._columns_sizes = {} # type: Dict[str, int]
+ self._columns_sizes: Dict[str, int] = {}
self._column_sel = ''
self._column_order = ''
self._column_order_asc = False
@@ -189,23 +188,24 @@ class ColumnHeaderWin(Win):
log.debug('Refresh: %s', self.__class__.__name__)
self._win.erase()
x = 0
+ theme = get_theme()
for col in self._columns:
txt = col
if col in self._column_order:
if self._column_order_asc:
- txt += get_theme().CHAR_COLUMN_ASC
+ txt += theme.CHAR_COLUMN_ASC
else:
- txt += get_theme().CHAR_COLUMN_DESC
+ txt += theme.CHAR_COLUMN_DESC
#⇓⇑↑↓⇧⇩▲▼
size = self._columns_sizes[col]
txt += ' ' * (size - len(txt))
if col in self._column_sel:
self.addstr(
0, x, txt,
- to_curses_attr(get_theme().COLOR_COLUMN_HEADER_SEL))
+ to_curses_attr(theme.COLOR_COLUMN_HEADER_SEL))
else:
self.addstr(0, x, txt,
- to_curses_attr(get_theme().COLOR_COLUMN_HEADER))
+ to_curses_attr(theme.COLOR_COLUMN_HEADER))
x += size
self._refresh()
diff --git a/poezio/windows/misc.py b/poezio/windows/misc.py
index 6c04b814..a621b61d 100644
--- a/poezio/windows/misc.py
+++ b/poezio/windows/misc.py
@@ -22,8 +22,10 @@ class VerticalSeparator(Win):
__slots__ = ()
def rewrite_line(self) -> None:
- self._win.vline(0, 0, curses.ACS_VLINE, self.height,
- to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR))
+ self._win.vline(
+ 0, 0, curses.ACS_VLINE, self.height,
+ to_curses_attr(get_theme().COLOR_VERTICAL_SEPARATOR)
+ ) # type: ignore
self._refresh()
def refresh(self) -> None:
@@ -37,7 +39,7 @@ class SimpleTextWin(Win):
def __init__(self, text) -> None:
Win.__init__(self)
self._text = text
- self.built_lines = [] # type: List[str]
+ self.built_lines: List[str] = []
def rebuild_text(self) -> None:
"""
diff --git a/poezio/windows/muc.py b/poezio/windows/muc.py
index 72dc602c..0e95ac1b 100644
--- a/poezio/windows/muc.py
+++ b/poezio/windows/muc.py
@@ -33,7 +33,7 @@ class UserList(Win):
def __init__(self) -> None:
Win.__init__(self)
self.pos = 0
- self.cache = [] # type: List[CachedUser]
+ self.cache: List[CachedUser] = []
def scroll_up(self) -> bool:
self.pos += self.height - 1
@@ -65,14 +65,14 @@ class UserList(Win):
def refresh(self, users: List[User]) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
- if config.get('hide_user_list'):
+ if config.getbool('hide_user_list'):
return # do not refresh if this win is hidden.
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
self._win.erase()
- asc_sort = (config.get('user_list_sort').lower() == 'asc')
+ asc_sort = (config.getstr('user_list_sort').lower() == 'asc')
if asc_sort:
y, _ = self._win.getmaxyx()
y -= 1
@@ -110,15 +110,16 @@ class UserList(Win):
self.addstr(y, 1, symbol, to_curses_attr(color))
def draw_status_chatstate(self, y: int, user: User) -> None:
- show_col = get_theme().color_show(user.show)
+ theme = get_theme()
+ show_col = theme.color_show(user.show)
if user.chatstate == 'composing':
- char = get_theme().CHAR_CHATSTATE_COMPOSING
+ char = theme.CHAR_CHATSTATE_COMPOSING
elif user.chatstate == 'active':
- char = get_theme().CHAR_CHATSTATE_ACTIVE
+ char = theme.CHAR_CHATSTATE_ACTIVE
elif user.chatstate == 'paused':
- char = get_theme().CHAR_CHATSTATE_PAUSED
+ char = theme.CHAR_CHATSTATE_PAUSED
else:
- char = get_theme().CHAR_STATUS
+ char = theme.CHAR_STATUS
self.addstr(y, 0, char, to_curses_attr(show_col))
def resize(self, height: int, width: int, y: int, x: int) -> None:
@@ -138,17 +139,18 @@ class Topic(Win):
def refresh(self, topic: Optional[str] = None) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
+ theme = get_theme()
self._win.erase()
if topic is not None:
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))
+ self.addstr(0, 0, msg, to_curses_attr(theme.COLOR_TOPIC_BAR))
_, 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))
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
self._refresh()
def set_message(self, message) -> None:
diff --git a/poezio/windows/roster_win.py b/poezio/windows/roster_win.py
index 3c62ea0a..dfdc9b9b 100644
--- a/poezio/windows/roster_win.py
+++ b/poezio/windows/roster_win.py
@@ -6,11 +6,10 @@ import logging
log = logging.getLogger(__name__)
from datetime import datetime
-from typing import Optional, List, Union, Dict
+from typing import Optional, List, Union
from poezio.windows.base_wins import Win
-from poezio import common
from poezio.config import config
from poezio.contact import Contact, Resource
from poezio.roster import Roster, RosterGroup
@@ -26,8 +25,8 @@ class RosterWin(Win):
Win.__init__(self)
self.pos = 0 # cursor position in the contact list
self.start_pos = 1 # position of the start of the display
- self.selected_row = None # type: Optional[Row]
- self.roster_cache = [] # type: List[Row]
+ self.selected_row: Optional[Row] = None
+ self.roster_cache: List[Row] = []
@property
def roster_len(self) -> int:
@@ -99,13 +98,13 @@ class RosterWin(Win):
# This is a search
if roster.contact_filter is not roster.DEFAULT_FILTER:
self.roster_cache = []
- sort = config.get('roster_sort', 'jid:show') or 'jid:show'
+ sort = config.getstr('roster_sort') or 'jid:show'
for contact in roster.get_contacts_sorted_filtered(sort):
self.roster_cache.append(contact)
else:
- show_offline = config.get('roster_show_offline')
- sort = config.get('roster_sort') or 'jid:show'
- group_sort = config.get('roster_group_sort') or 'name'
+ show_offline = config.getbool('roster_show_offline')
+ sort = config.getstr('roster_sort') or 'jid:show'
+ group_sort = config.getstr('roster_group_sort') or 'name'
self.roster_cache = []
# build the cache
for group in roster.get_groups(group_sort):
@@ -155,9 +154,9 @@ class RosterWin(Win):
self.height]
options = {
- 'show_roster_sub': config.get('show_roster_subscriptions'),
- 'show_s2s_errors': config.get('show_s2s_errors'),
- 'show_roster_jids': config.get('show_roster_jids')
+ 'show_roster_sub': config.getbool('show_roster_subscriptions'),
+ 'show_s2s_errors': config.getbool('show_s2s_errors'),
+ 'show_roster_jids': config.getbool('show_roster_jids')
}
for item in roster_view:
@@ -171,7 +170,7 @@ class RosterWin(Win):
group = item.name
elif isinstance(item, Contact):
self.draw_contact_line(y, item, draw_selected, group,
- **options)
+ **options) # type: ignore
elif isinstance(item, Resource):
self.draw_resource_line(y, item, draw_selected)
@@ -195,18 +194,20 @@ class RosterWin(Win):
"""
The header at the top
"""
+ color = get_theme().COLOR_INFORMATION_BAR
self.addstr(
'Roster: %s/%s contacts' % (roster.get_nb_connected_contacts(),
len(roster)),
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
- self.finish_line(get_theme().COLOR_INFORMATION_BAR)
+ to_curses_attr(color))
+ self.finish_line(color)
def draw_group(self, y: int, group: RosterGroup, colored: bool) -> None:
"""
Draw a groupname on a line
"""
+ color = to_curses_attr(get_theme().COLOR_SELECTED_ROW)
if colored:
- self._win.attron(to_curses_attr(get_theme().COLOR_SELECTED_ROW))
+ self._win.attron(color)
if group.folded:
self.addstr(y, 0, '[+] ')
else:
@@ -217,7 +218,7 @@ class RosterWin(Win):
self.truncate_name(group.name,
len(contacts) + 4) + contacts)
if colored:
- self._win.attroff(to_curses_attr(get_theme().COLOR_SELECTED_ROW))
+ self._win.attroff(color)
self.finish_line()
def truncate_name(self, name, added):
@@ -263,17 +264,9 @@ class RosterWin(Win):
added += 4
if contact.ask:
- added += len(get_theme().CHAR_ROSTER_ASKED)
+ added += len(theme.CHAR_ROSTER_ASKED)
if show_s2s_errors and contact.error:
- added += len(get_theme().CHAR_ROSTER_ERROR)
- if contact.tune:
- added += len(get_theme().CHAR_ROSTER_TUNE)
- if contact.mood:
- added += len(get_theme().CHAR_ROSTER_MOOD)
- if contact.activity:
- added += len(get_theme().CHAR_ROSTER_ACTIVITY)
- if contact.gaming:
- added += len(get_theme().CHAR_ROSTER_GAMING)
+ added += len(theme.CHAR_ROSTER_ERROR)
if show_roster_sub in ('all', 'incomplete', 'to', 'from', 'both',
'none'):
added += len(
@@ -285,13 +278,13 @@ class RosterWin(Win):
elif contact.name and contact.name != contact.bare_jid:
display_name = '%s (%s)' % (contact.name, contact.bare_jid)
else:
- display_name = contact.bare_jid
+ display_name = str(contact.bare_jid)
display_name = self.truncate_name(display_name, added) + nb
if colored:
self.addstr(display_name,
- to_curses_attr(get_theme().COLOR_SELECTED_ROW))
+ to_curses_attr(theme.COLOR_SELECTED_ROW))
else:
self.addstr(display_name)
@@ -302,34 +295,23 @@ class RosterWin(Win):
contact.subscription, keep=show_roster_sub),
to_curses_attr(theme.COLOR_ROSTER_SUBSCRIPTION))
if contact.ask:
- self.addstr(get_theme().CHAR_ROSTER_ASKED,
- to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT))
+ self.addstr(theme.CHAR_ROSTER_ASKED,
+ to_curses_attr(theme.COLOR_IMPORTANT_TEXT))
if show_s2s_errors and contact.error:
- self.addstr(get_theme().CHAR_ROSTER_ERROR,
- to_curses_attr(get_theme().COLOR_ROSTER_ERROR))
- if contact.tune:
- self.addstr(get_theme().CHAR_ROSTER_TUNE,
- to_curses_attr(get_theme().COLOR_ROSTER_TUNE))
- if contact.activity:
- self.addstr(get_theme().CHAR_ROSTER_ACTIVITY,
- to_curses_attr(get_theme().COLOR_ROSTER_ACTIVITY))
- if contact.mood:
- self.addstr(get_theme().CHAR_ROSTER_MOOD,
- to_curses_attr(get_theme().COLOR_ROSTER_MOOD))
- if contact.gaming:
- self.addstr(get_theme().CHAR_ROSTER_GAMING,
- to_curses_attr(get_theme().COLOR_ROSTER_GAMING))
+ self.addstr(theme.CHAR_ROSTER_ERROR,
+ to_curses_attr(theme.COLOR_ROSTER_ERROR))
self.finish_line()
def draw_resource_line(self, y: int, resource: Resource, colored: bool) -> None:
"""
Draw a specific resource line
"""
- color = get_theme().color_show(resource.presence)
- self.addstr(y, 4, get_theme().CHAR_STATUS, to_curses_attr(color))
+ theme = get_theme()
+ color = theme.color_show(resource.presence)
+ self.addstr(y, 4, theme.CHAR_STATUS, to_curses_attr(color))
if colored:
self.addstr(y, 8, self.truncate_name(str(resource.jid), 6),
- to_curses_attr(get_theme().COLOR_SELECTED_ROW))
+ to_curses_attr(theme.COLOR_SELECTED_ROW))
else:
self.addstr(y, 8, self.truncate_name(str(resource.jid), 6))
self.finish_line()
@@ -350,6 +332,7 @@ class ContactInfoWin(Win):
"""
draw the contact information
"""
+ theme = get_theme()
resource = contact.get_highest_priority_resource()
if contact:
jid = str(contact.bare_jid)
@@ -365,8 +348,8 @@ class ContactInfoWin(Win):
self.addstr(0, 0, '%s (%s)' % (
jid,
presence,
- ), to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
- self.finish_line(get_theme().COLOR_INFORMATION_BAR)
+ ), to_curses_attr(theme.COLOR_INFORMATION_BAR))
+ self.finish_line(theme.COLOR_INFORMATION_BAR)
i += 1
self.addstr(i, 0, 'Subscription: %s' % (contact.subscription, ))
self.finish_line()
@@ -374,7 +357,7 @@ class ContactInfoWin(Win):
if contact.ask:
if contact.ask == 'asked':
self.addstr(i, 0, 'Ask: %s' % (contact.ask, ),
- to_curses_attr(get_theme().COLOR_IMPORTANT_TEXT))
+ to_curses_attr(theme.COLOR_IMPORTANT_TEXT))
else:
self.addstr(i, 0, 'Ask: %s' % (contact.ask, ))
self.finish_line()
@@ -386,33 +369,7 @@ class ContactInfoWin(Win):
if contact.error:
self.addstr(i, 0, 'Error: %s' % contact.error,
- to_curses_attr(get_theme().COLOR_ROSTER_ERROR))
- self.finish_line()
- i += 1
-
- if contact.tune:
- self.addstr(i, 0,
- 'Tune: %s' % common.format_tune_string(contact.tune),
- to_curses_attr(get_theme().COLOR_NORMAL_TEXT))
- self.finish_line()
- i += 1
-
- if contact.mood:
- self.addstr(i, 0, 'Mood: %s' % contact.mood,
- to_curses_attr(get_theme().COLOR_NORMAL_TEXT))
- self.finish_line()
- i += 1
-
- if contact.activity:
- self.addstr(i, 0, 'Activity: %s' % contact.activity,
- to_curses_attr(get_theme().COLOR_NORMAL_TEXT))
- self.finish_line()
- i += 1
-
- if contact.gaming:
- self.addstr(
- i, 0, 'Game: %s' % common.format_gaming_string(contact.gaming),
- to_curses_attr(get_theme().COLOR_NORMAL_TEXT))
+ to_curses_attr(theme.COLOR_ROSTER_ERROR))
self.finish_line()
i += 1
@@ -420,9 +377,10 @@ class ContactInfoWin(Win):
"""
draw the group information
"""
+ theme = get_theme()
self.addstr(0, 0, group.name,
- to_curses_attr(get_theme().COLOR_INFORMATION_BAR))
- self.finish_line(get_theme().COLOR_INFORMATION_BAR)
+ to_curses_attr(theme.COLOR_INFORMATION_BAR))
+ self.finish_line(theme.COLOR_INFORMATION_BAR)
def refresh(self, selected_row: Row) -> None:
log.debug('Refresh: %s', self.__class__.__name__)
diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py
index d0669b26..12d90e7d 100644
--- a/poezio/windows/text_win.py
+++ b/poezio/windows/text_win.py
@@ -4,50 +4,50 @@ Can be locked, scrolled, has a separator, etc…
"""
import logging
-import curses
-from math import ceil, log10
from typing import Optional, List, Union
-from poezio.windows.base_wins import Win, FORMAT_CHAR
-from poezio.windows.funcs import truncate_nick, parse_attrs
+from poezio.windows.base_wins import Win
+from poezio.text_buffer import TextBuffer
-from poezio import poopt
from poezio.config import config
-from poezio.theming import to_curses_attr, get_theme, dump_tuple
-from poezio.text_buffer import Message
+from poezio.theming import to_curses_attr, get_theme
+from poezio.ui.types import Message, BaseMessage
+from poezio.ui.render import Line, build_lines, write_pre
log = logging.getLogger(__name__)
-# msg is a reference to the corresponding Message object. text_start and
-# text_end are the position delimiting the text in this line.
-class Line:
- __slots__ = ('msg', 'start_pos', 'end_pos', 'prepend')
-
- def __init__(self, msg: Message, start_pos: int, end_pos: int, prepend: str) -> None:
- self.msg = msg
- self.start_pos = start_pos
- self.end_pos = end_pos
- self.prepend = prepend
-
-
-class BaseTextWin(Win):
+class TextWin(Win):
__slots__ = ('lines_nb_limit', 'pos', 'built_lines', 'lock', 'lock_buffer',
- 'separator_after')
+ 'separator_after', 'highlights', 'hl_pos',
+ 'nb_of_highlights_after_separator')
+
+ hl_pos: Optional[int]
def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
- if lines_nb_limit is None:
- lines_nb_limit = config.get('max_lines_in_memory')
Win.__init__(self)
- self.lines_nb_limit = lines_nb_limit # type: int
+ if lines_nb_limit is None:
+ lines_nb_limit = config.getint('max_lines_in_memory')
+ self.lines_nb_limit: int = lines_nb_limit
self.pos = 0
# Each new message is built and kept here.
# on resize, we rebuild all the messages
- self.built_lines = [] # type: List[Union[None, Line]]
+ self.built_lines: List[Union[None, Line]] = []
self.lock = False
- self.lock_buffer = [] # type: List[Union[None, Line]]
- self.separator_after = None # type: Optional[Line]
+ self.lock_buffer: List[Union[None, Line]] = []
+ self.separator_after: Optional[BaseMessage] = None
+ # the Lines of the highlights in that buffer
+ self.highlights: List[Line] = []
+ # 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 = None
+
+ # 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
def toggle_lock(self) -> bool:
if self.lock:
@@ -80,12 +80,9 @@ class BaseTextWin(Win):
self.pos = 0
return self.pos != pos
- # TODO: figure out the type of history.
def build_new_message(self,
- message: Message,
- history=None,
+ message: BaseMessage,
clean: bool = True,
- highlight: bool = False,
timestamp: bool = False,
nick_size: int = 10) -> int:
"""
@@ -93,29 +90,55 @@ class BaseTextWin(Win):
Return the number of lines that are built for the given
message.
"""
- #pylint: disable=assignment-from-no-return
- lines = self.build_message(
- message, timestamp=timestamp, nick_size=nick_size)
+ lines = build_lines(
+ message, self.width, timestamp=timestamp, nick_size=nick_size
+ )
if self.lock:
self.lock_buffer.extend(lines)
else:
self.built_lines.extend(lines)
if not lines or not lines[0]:
return 0
+ if isinstance(message, Message) and message.highlight:
+ self.highlights.append(lines[0])
+ self.nb_of_highlights_after_separator += 1
+ log.debug("Number of highlights after separator is now %s",
+ self.nb_of_highlights_after_separator)
if clean:
while len(self.built_lines) > self.lines_nb_limit:
self.built_lines.pop(0)
return len(lines)
- def build_message(self, message: Message, timestamp: bool = False, nick_size: int = 10) -> List[Union[None, Line]]:
- """
- Build a list of lines from a message, without adding it
- to a list
- """
- return []
-
def refresh(self) -> None:
- pass
+ log.debug('Refresh: %s', self.__class__.__name__)
+ 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]
+ with_timestamps = config.getbool("show_timestamps")
+ nick_size = config.getint("max_nick_length")
+ self._win.move(0, 0)
+ self._win.erase()
+ offset = 0
+ for y, line in enumerate(lines):
+ if line:
+ msg = line.msg
+ if line.start_pos == 0:
+ offset = write_pre(msg, self, with_timestamps, nick_size)
+ elif y == 0:
+ offset = msg.compute_offset(with_timestamps,
+ nick_size)
+ self.write_text(
+ y, offset,
+ line.prepend + line.msg.txt[line.start_pos:line.end_pos])
+ else:
+ self.write_line_separator(y)
+ if y != self.height - 1:
+ self.addstr('\n')
+ self._win.attrset(0)
+ self._refresh()
def write_text(self, y: int, x: int, txt: str) -> None:
"""
@@ -123,28 +146,15 @@ class BaseTextWin(Win):
"""
self.addstr_colored(txt, y, x)
- def write_time(self, time: str) -> int:
- """
- Write the date on the yth line of the window
- """
- if time:
- color = get_theme().COLOR_TIME_STRING
- curses_color = to_curses_attr(color)
- self._win.attron(curses_color)
- self.addstr(time)
- self._win.attroff(curses_color)
- self.addstr(' ')
- return poopt.wcswidth(time) + 1
- return 0
-
- # TODO: figure out the type of room.
- def resize(self, height: int, width: int, y: int, x: int, room=None) -> None:
+ def resize(self, height: int, width: int, y: int, x: int,
+ room: Optional[TextBuffer] = None, force: bool = False) -> None:
+ old_width: Optional[int]
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:
+ if room and (self.width != old_width or force):
self.rebuild_everything(room)
# reposition the scrolling after resize
@@ -155,11 +165,10 @@ class BaseTextWin(Win):
if self.pos < 0:
self.pos = 0
- # TODO: figure out the type of room.
- def rebuild_everything(self, room) -> None:
+ def rebuild_everything(self, room: TextBuffer) -> None:
self.built_lines = []
- with_timestamps = config.get('show_timestamps')
- nick_size = config.get('max_nick_length')
+ with_timestamps = config.getbool('show_timestamps')
+ nick_size = config.getint('max_nick_length')
for message in room.messages:
self.build_new_message(
message,
@@ -167,34 +176,43 @@ class BaseTextWin(Win):
timestamp=with_timestamps,
nick_size=nick_size)
if self.separator_after is message:
- self.build_new_message(None)
+ self.built_lines.append(None)
while len(self.built_lines) > self.lines_nb_limit:
self.built_lines.pop(0)
+ def remove_line_separator(self) -> None:
+ """
+ Remove the line separator
+ """
+ log.debug('remove_line_separator')
+ if None in self.built_lines:
+ self.built_lines.remove(None)
+ self.separator_after = None
+
+ def add_line_separator(self, room: TextBuffer = None) -> None:
+ """
+ add a line separator at the end of messages list
+ room is a textbuffer that is needed to get the previous message
+ (in case of resize)
+ """
+ if None not in self.built_lines:
+ self.built_lines.append(None)
+ self.nb_of_highlights_after_separator = 0
+ log.debug("Resetting number of highlights after separator")
+ if room and room.messages:
+ self.separator_after = room.messages[-1]
+
+ def write_line_separator(self, y) -> None:
+ theme = get_theme()
+ char = theme.CHAR_NEW_TEXT_SEPARATOR
+ self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
+ to_curses_attr(theme.COLOR_NEW_TEXT_SEPARATOR))
+
def __del__(self) -> None:
log.debug('** TextWin: deleting %s built lines',
(len(self.built_lines)))
del self.built_lines
-
-class TextWin(BaseTextWin):
- __slots__ = ('highlights', 'hl_pos', 'nb_of_highlights_after_separator')
-
- def __init__(self, lines_nb_limit: Optional[int] = None) -> None:
- BaseTextWin.__init__(self, lines_nb_limit)
-
- # the Lines of the highlights in that buffer
- self.highlights = [] # type: List[Line]
- # 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
-
def next_highlight(self) -> None:
"""
Go to the next highlight in the buffer.
@@ -203,13 +221,13 @@ class TextWin(BaseTextWin):
highlights, scroll to the end of the buffer.
"""
log.debug('Going to the next highlight…')
- if (not self.highlights or self.hl_pos != self.hl_pos
+ if (not self.highlights or self.hl_pos is None
or self.hl_pos >= len(self.highlights) - 1):
- self.hl_pos = float('nan')
+ self.hl_pos = None
self.pos = 0
return
hl_size = len(self.highlights) - 1
- if self.hl_pos < hl_size:
+ if self.hl_pos is not None and self.hl_pos < hl_size:
self.hl_pos += 1
else:
self.hl_pos = hl_size
@@ -220,9 +238,10 @@ class TextWin(BaseTextWin):
try:
pos = self.built_lines.index(hl)
except ValueError:
- self.highlights = self.highlights[self.hl_pos + 1:]
+ if isinstance(self.hl_pos, int):
+ del self.highlights[self.hl_pos]
if not self.highlights:
- self.hl_pos = float('nan')
+ self.hl_pos = None
self.pos = 0
return
self.hl_pos = 0
@@ -239,11 +258,11 @@ class TextWin(BaseTextWin):
highlights, scroll to the end of the buffer.
"""
log.debug('Going to the previous highlight…')
- if not self.highlights or self.hl_pos <= 0:
- self.hl_pos = float('nan')
+ if not self.highlights or self.hl_pos and self.hl_pos <= 0:
+ self.hl_pos = None
self.pos = 0
return
- if self.hl_pos != self.hl_pos:
+ if self.hl_pos is None:
self.hl_pos = len(self.highlights) - 1
else:
self.hl_pos -= 1
@@ -254,9 +273,10 @@ class TextWin(BaseTextWin):
try:
pos = self.built_lines.index(hl)
except ValueError:
- self.highlights = self.highlights[self.hl_pos + 1:]
+ if self.hl_pos is not None:
+ del self.highlights[self.hl_pos]
if not self.highlights:
- self.hl_pos = float('nan')
+ self.hl_pos = None
self.pos = 0
return
self.hl_pos = 0
@@ -267,8 +287,8 @@ class TextWin(BaseTextWin):
def scroll_to_separator(self) -> None:
"""
- Scroll until separator is centered. If no separator is
- present, scroll at the top of the window
+ Scroll to the first message after the separator. If no
+ separator is present, scroll to the first message of the window
"""
if None in self.built_lines:
self.pos = len(self.built_lines) - self.built_lines.index(
@@ -286,371 +306,31 @@ class TextWin(BaseTextWin):
self.highlights) - self.nb_of_highlights_after_separator - 1
log.debug("self.hl_pos = %s", self.hl_pos)
- def remove_line_separator(self) -> None:
- """
- Remove the line separator
- """
- log.debug('remove_line_separator')
- if None in self.built_lines:
- self.built_lines.remove(None)
- self.separator_after = None
-
- # TODO: figure out the type of room.
- def add_line_separator(self, room=None) -> None:
- """
- add a line separator at the end of messages list
- room is a textbuffer that is needed to get the previous message
- (in case of resize)
- """
- if None not in self.built_lines:
- self.built_lines.append(None)
- self.nb_of_highlights_after_separator = 0
- log.debug("Resetting number of highlights after separator")
- if room and room.messages:
- self.separator_after = room.messages[-1]
-
- # TODO: figure out the type of history.
- def build_new_message(self,
- message: Message,
- history=None,
- clean: bool = True,
- highlight: bool = False,
- timestamp: bool = False,
- nick_size: int = 10) -> int:
- """
- 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, nick_size=nick_size)
- if self.lock:
- self.lock_buffer.extend(lines)
- else:
- self.built_lines.extend(lines)
- if not lines or not lines[0]:
- return 0
- if highlight:
- self.highlights.append(lines[0])
- self.nb_of_highlights_after_separator += 1
- log.debug("Number of highlights after separator is now %s",
- self.nb_of_highlights_after_separator)
- if clean:
- while len(self.built_lines) > self.lines_nb_limit:
- self.built_lines.pop(0)
- return len(lines)
-
- def build_message(self, message: Optional[Message], timestamp: bool = False, nick_size: int = 10) -> List[Union[None, Line]]:
- """
- Build a list of lines from a message, without adding it
- to a list
- """
- if message is None: # line separator
- return [None]
- txt = message.txt
- if not txt:
- return []
- if len(message.str_time) > 8:
- default_color = (
- FORMAT_CHAR + dump_tuple(get_theme().COLOR_LOG_MSG) + '}') # type: Optional[str]
- else:
- default_color = None
- ret = [] # type: List[Union[None, Line]]
- nick = truncate_nick(message.nickname, nick_size)
- offset = 0
- if message.ack:
- if message.ack > 0:
- offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
- else:
- offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1
- if nick:
- offset += poopt.wcswidth(nick) + 2 # + nick + '> ' length
- if message.revisions > 0:
- offset += ceil(log10(message.revisions + 1))
- if message.me:
- offset += 1 # '* ' before and ' ' after
- if timestamp:
- 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 = [] # type: List[str]
- 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 refresh(self) -> None:
- log.debug('Refresh: %s', self.__class__.__name__)
- 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]
- with_timestamps = config.get("show_timestamps")
- nick_size = config.get("max_nick_length")
- self._win.move(0, 0)
- self._win.erase()
- offset = 0
- for y, line in enumerate(lines):
- if line:
- msg = line.msg
- if line.start_pos == 0:
- offset = self.write_pre_msg(msg, with_timestamps,
- nick_size)
- elif y == 0:
- offset = self.compute_offset(msg, with_timestamps,
- nick_size)
- self.write_text(
- y, offset,
- line.prepend + line.msg.txt[line.start_pos:line.end_pos])
- else:
- self.write_line_separator(y)
- if y != self.height - 1:
- self.addstr('\n')
- self._win.attrset(0)
- self._refresh()
-
- def compute_offset(self, msg, with_timestamps, nick_size) -> int:
- offset = 0
- if with_timestamps and msg.str_time:
- offset += poopt.wcswidth(msg.str_time) + 1
-
- if not msg.nickname: # not a message, nothing to do afterwards
- return offset
-
- nick = truncate_nick(msg.nickname, nick_size)
- offset += poopt.wcswidth(nick)
- if msg.ack:
- if msg.ack > 0:
- offset += poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
- else:
- offset += poopt.wcswidth(get_theme().CHAR_NACK) + 1
- if msg.me:
- offset += 3
- else:
- offset += 2
- if msg.revisions:
- offset += ceil(log10(msg.revisions + 1))
- offset += self.write_revisions(msg)
- return offset
-
- def write_pre_msg(self, msg, with_timestamps, nick_size) -> int:
- offset = 0
- if with_timestamps:
- offset += self.write_time(msg.str_time)
-
- if not msg.nickname: # not a message, nothing to do afterwards
- return offset
-
- nick = truncate_nick(msg.nickname, nick_size)
- offset += poopt.wcswidth(nick)
- if msg.nick_color:
- color = msg.nick_color
- elif msg.user:
- color = msg.user.color
- else:
- color = None
- if msg.ack:
- if msg.ack > 0:
- offset += self.write_ack()
- else:
- offset += self.write_nack()
- if msg.me:
- self._win.attron(to_curses_attr(get_theme().COLOR_ME_MESSAGE))
- self.addstr('* ')
- self.write_nickname(nick, color, msg.highlight)
- offset += self.write_revisions(msg)
- self.addstr(' ')
- offset += 3
- else:
- self.write_nickname(nick, color, msg.highlight)
- offset += self.write_revisions(msg)
- self.addstr('> ')
- offset += 2
- return offset
-
- def write_revisions(self, msg) -> int:
- if msg.revisions:
- self._win.attron(
- to_curses_attr(get_theme().COLOR_REVISIONS_MESSAGE))
- self.addstr('%d' % msg.revisions)
- self._win.attrset(0)
- return ceil(log10(msg.revisions + 1))
- return 0
-
- def write_line_separator(self, y) -> None:
- char = get_theme().CHAR_NEW_TEXT_SEPARATOR
- self.addnstr(y, 0, char * (self.width // len(char) - 1), self.width,
- to_curses_attr(get_theme().COLOR_NEW_TEXT_SEPARATOR))
-
- def write_ack(self) -> int:
- color = get_theme().COLOR_CHAR_ACK
- self._win.attron(to_curses_attr(color))
- self.addstr(get_theme().CHAR_ACK_RECEIVED)
- self._win.attroff(to_curses_attr(color))
- self.addstr(' ')
- return poopt.wcswidth(get_theme().CHAR_ACK_RECEIVED) + 1
-
- def write_nack(self) -> int:
- color = get_theme().COLOR_CHAR_NACK
- self._win.attron(to_curses_attr(color))
- self.addstr(get_theme().CHAR_NACK)
- self._win.attroff(to_curses_attr(color))
- self.addstr(' ')
- return poopt.wcswidth(get_theme().CHAR_NACK) + 1
-
- def write_nickname(self, nickname, color, highlight=False) -> None:
- """
- Write the nickname, using the user's color
- and return the number of written characters
- """
- if not nickname:
- return
- if highlight:
- hl_color = get_theme().COLOR_HIGHLIGHT_NICK
- if hl_color == "reverse":
- self._win.attron(curses.A_REVERSE)
- else:
- color = hl_color
- if color:
- self._win.attron(to_curses_attr(color))
- self.addstr(nickname)
- if color:
- self._win.attroff(to_curses_attr(color))
- if highlight and hl_color == "reverse":
- self._win.attroff(curses.A_REVERSE)
-
def modify_message(self, old_id, message) -> None:
"""
Find a message, and replace it with a new one
(instead of rebuilding everything in order to correct a message)
"""
- with_timestamps = config.get('show_timestamps')
- nick_size = config.get('max_nick_length')
+ with_timestamps = config.getbool('show_timestamps')
+ nick_size = config.getint('max_nick_length')
for i in range(len(self.built_lines) - 1, -1, -1):
- if self.built_lines[i] and self.built_lines[i].msg.identifier == old_id:
+ current = self.built_lines[i]
+ if current is not None and current.msg.identifier == old_id:
index = i
- while index >= 0 and self.built_lines[index] and self.built_lines[index].msg.identifier == old_id:
+ while (
+ index >= 0
+ and current is not None
+ and current.msg.identifier == old_id
+ ):
self.built_lines.pop(index)
index -= 1
+ if index >= 0:
+ current = self.built_lines[index]
index += 1
- lines = self.build_message(
- message, timestamp=with_timestamps, nick_size=nick_size)
+ lines = build_lines(
+ message, self.width, timestamp=with_timestamps, nick_size=nick_size
+ )
for line in lines:
self.built_lines.insert(index, line)
index += 1
break
-
- def __del__(self) -> None:
- log.debug('** TextWin: deleting %s built lines',
- (len(self.built_lines)))
- del self.built_lines
-
-
-class XMLTextWin(BaseTextWin):
- __slots__ = ()
-
- def __init__(self) -> None:
- BaseTextWin.__init__(self)
-
- def refresh(self) -> None:
- 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: Message, timestamp: bool = False, nick_size: int = 10) -> List[Line]:
- txt = message.txt
- ret = []
- default_color = None
- nick = truncate_nick(message.nickname, nick_size)
- 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 = [] # type: List[str]
- 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) -> None:
- self._win.attron(to_curses_attr(color))
- self.addstr(truncate_nick(nickname))
- self._win.attroff(to_curses_attr(color))