summaryrefslogtreecommitdiff
path: root/poezio/windows/base_wins.py
blob: 658e153393b07c43bddba947e2b889be8deb0ffe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
"""
Define the base window object and the constants/"globals" used
by the file of this module.

A window is a little part of the screen, for example the input window,
the text window, the roster window, etc.
A Tab (see the poezio.tabs module) is composed of multiple Windows
"""

from __future__ import annotations

import curses
import logging
import string

from contextlib import contextmanager
from typing import Optional, Tuple, TYPE_CHECKING, cast

from poezio.theming import to_curses_attr, read_tuple

from poezio.ui.consts import FORMAT_CHAR

log = logging.getLogger(__name__)

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:
        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:
        if height == 0 or width == 0:
            self.height, self.width = height, width
            return
        self.height, self.width, self.x, self.y = height, width, x, y
        try:
            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')

    def resize(self, height: int, width: int, y: int, x: int) -> None:
        """
        Override if something has to be done on resize
        """
        self._resize(height, width, y, x)

    def _refresh(self) -> None:
        self._win.noutrefresh()

    def addnstr(self, *args) -> None:
        """
        Safe call to addnstr
        """
        try:
            self._win.addnstr(*args)
        except:
            # this actually mostly returns ERR, but works.
            # more specifically, when the added string reaches the end
            # of the screen.
            pass

    @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
        """
        try:
            self._win.addstr(*args)
        except:
            pass

    def move(self, y: int, x: int) -> None:
        try:
            self._win.move(y, x)
        except:
            pass

    def addstr_colored(self, text: str, y: Optional[int] = None, x: Optional[int] = None) -> None:
        """
        Write a string on the window, setting the
        attributes as they are in the string.
        For example:
        \x19bhello → hello in bold
        \x191}Bonj\x192}our → 'Bonj' in red and 'our' in green
        next_attr_char is the \x19 delimiter
        attr_char is the char following it, it can be
        one of 'u', 'b', 'i', 'c[0-9]'
        """
        if y is not None and x is not None:
            self.move(y, x)
        next_attr_char = text.find(FORMAT_CHAR)
        attr_italic = curses.A_ITALIC if hasattr(
            curses, 'A_ITALIC') else curses.A_REVERSE
        while next_attr_char != -1 and text:
            if next_attr_char + 1 < len(text):
                attr_char = text[next_attr_char + 1].lower()
            else:
                attr_char = str()
            if next_attr_char != 0:
                self.addstr(text[:next_attr_char])
            if attr_char == 'o':
                self._win.attrset(0)
            elif attr_char == 'u':
                self._win.attron(curses.A_UNDERLINE)
            elif attr_char == 'b':
                self._win.attron(curses.A_BOLD)
            elif attr_char == 'i':
                self._win.attron(attr_italic)
            if (attr_char in string.digits
                    or attr_char == '-') and attr_char != '':
                color_str = text[next_attr_char +
                                 1:text.find('}', next_attr_char)]
                if ',' in color_str:
                    tup, char = read_tuple(color_str)
                    self._win.attron(to_curses_attr(tup))
                    if char:
                        if char == 'o':
                            self._win.attrset(0)
                        elif char == 'u':
                            self._win.attron(curses.A_UNDERLINE)
                        elif char == 'b':
                            self._win.attron(curses.A_BOLD)
                        elif char == 'i':
                            self._win.attron(attr_italic)
                    else:
                        # this will reset previous bold/uderline sequences if any was used
                        self._win.attroff(curses.A_UNDERLINE)
                        self._win.attroff(curses.A_BOLD)
                elif color_str:
                    self._win.attron(to_curses_attr((int(color_str), -1)))
                text = text[next_attr_char + len(color_str) + 2:]
            else:
                text = text[next_attr_char + 2:]
            next_attr_char = text.find(FORMAT_CHAR)
        self.addstr(text)

    def finish_line(self, color: Optional[Tuple] = None) -> None:
        """
        Write colored spaces until the end of line
        """
        (y, x) = self._win.getyx()
        size = self.width - x
        if color:
            self.addnstr(' ' * size, size, to_curses_attr(color))
        else:
            self.addnstr(' ' * size, size)


TAB_WIN: Optional[_CursesWindow] = None