diff options
Diffstat (limited to 'poezio/windows')
-rw-r--r-- | poezio/windows/__init__.py | 4 | ||||
-rw-r--r-- | poezio/windows/base_wins.py | 60 | ||||
-rw-r--r-- | poezio/windows/bookmark_forms.py | 96 | ||||
-rw-r--r-- | poezio/windows/data_forms.py | 39 | ||||
-rw-r--r-- | poezio/windows/funcs.py | 60 | ||||
-rw-r--r-- | poezio/windows/image.py | 56 | ||||
-rw-r--r-- | poezio/windows/info_bar.py | 103 | ||||
-rw-r--r-- | poezio/windows/info_wins.py | 153 | ||||
-rw-r--r-- | poezio/windows/input_placeholders.py | 2 | ||||
-rw-r--r-- | poezio/windows/inputs.py | 50 | ||||
-rw-r--r-- | poezio/windows/list.py | 24 | ||||
-rw-r--r-- | poezio/windows/misc.py | 8 | ||||
-rw-r--r-- | poezio/windows/muc.py | 22 | ||||
-rw-r--r-- | poezio/windows/roster_win.py | 116 | ||||
-rw-r--r-- | poezio/windows/text_win.py | 578 |
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)) |