class _Widget(ABC):
'Defines most basic TUI object API.'
+ @abstractmethod
+ def __init__(self, *args, **kwargs) -> None:
+ self.tainted = True
+
@abstractmethod
def set_geometry(self, measurements: _YX) -> None:
'Update widget\'s measurements, re-generate content where necessary.'
+ self.tainted = True
@abstractmethod
def draw(self) -> None:
'Print widget\'s content in shape appropriate to set geometry.'
+ self.tainted = False
class _ScrollableWidget(_Widget, ABC):
@abstractmethod
def _scroll(self, up=True) -> None:
- pass
+ self.tainted = True
def cmd__scroll(self, direction: str) -> None:
'Scroll through stored content/history.'
self._scroll(up=direction == 'up')
- self.draw()
class _LogWidget(_ScrollableWidget):
return len(wrapped_lines)
def set_geometry(self, measurements: _YX) -> None:
+ super().set_geometry(measurements)
self._view_size = measurements
self._y_pgscroll = self._view_size.y // 2
self._wrapped.clear()
if self._wrapped_idx < -1:
self._history_idx -= 1
self._wrapped_idx -= n_wrapped_lines
+ self.tainted = True
def draw(self) -> None:
+ super().draw()
start_idx = self._wrapped_idx - self._view_size.y + 1
end_idx = self._wrapped_idx
to_write = [t[1] for t in self._wrapped[start_idx:end_idx]]
self._write(line, i)
def _scroll(self, up: bool = True) -> None:
+ super()._scroll(up)
if up:
self._wrapped_idx = max(self._view_size.y + 1 - len(self._wrapped),
self._wrapped_idx - self._y_pgscroll)
_width: int
_prompt: str = _PROMPT_TEMPLATE
_history_idx = 0
- _input_buffer: str
+ _input_buffer_unsafe: str
_cursor_x: int
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._reset_buffer('')
+ @property
+ def _input_buffer(self) -> str:
+ return self._input_buffer_unsafe[:]
+
+ @_input_buffer.setter
+ def _input_buffer(self, content) -> None:
+ self.tainted = True
+ self._input_buffer_unsafe = content
+
def set_geometry(self, measurements: _YX) -> None:
+ super().set_geometry(measurements)
self._y, self._width = measurements
def draw(self) -> None:
+ super().draw()
prefix = self._prompt[:]
- content = self._input_buffer[:]
+ content = self._input_buffer
if self._cursor_x == len(self._input_buffer):
content += ' '
half_width = (self._width - len(prefix)) // 2
self._write(to_write[cursor_x_to_write + 1:])
def _archive_prompt(self) -> None:
- self.append(self._input_buffer[:])
+ self.append(self._input_buffer)
self._reset_buffer('')
def _scroll(self, up: bool = True) -> None:
+ super()._scroll(up)
if up and -(self._history_idx) < len(self._history):
if self._history_idx == 0 and self._input_buffer:
self._archive_prompt()
+ to_append
+ self._input_buffer[self._cursor_x - 1:])
self._history_idx = 0
- self.draw()
def cmd__backspace(self) -> None:
'Truncate current content by one character, if possible.'
self._input_buffer = (self._input_buffer[:self._cursor_x]
+ self._input_buffer[self._cursor_x + 1:])
self._history_idx = 0
- self.draw()
def cmd__move_cursor(self, direction: str) -> None:
'Move cursor one space into direction ("left" or "right") if possible.'
self._cursor_x += 1
else:
return
- self.draw()
+ self.tainted = True
def _reset_buffer(self, content: str) -> None:
self._input_buffer = content
to_return = self._input_buffer[:]
if to_return:
self._archive_prompt()
- self.draw()
return to_return
nick: Optional[str] = None
) -> None:
'Update nickname-relevant knowledge to go into prompt string.'
+ self.tainted = True
self._prompt = ''
if nick:
self._nickname = nick
self.set_geometry()
def set_geometry(self, _=None) -> None:
+ super().set_geometry(_)
assert _ is None
self._y_status = self._term.size.y - 2
self.log.set_geometry(_YX(self._y_status, self._term.size.x))
self.prompt.set_geometry(_YX(self._term.size.y - 1, self._term.size.x))
def draw(self) -> None:
+ super().draw()
idx_box = f'[{self.idx}]'
status_line = idx_box + '=' * (self._term.size.x - len(idx_box))
self._term.clear()
'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.'
self._term.write(f'\033{_OSC52_PREFIX}?{_PASTE_DELIMITER}',
self._y_status)
- self.draw()
+ self.tainted = True
+
+ def draw_tainted(self) -> None:
+ 'Draw tainted parts of self.'
+ if self.tainted:
+ self.draw()
+ return
+ for widget in [w for w in (self.log, self.prompt) if w.tainted]:
+ widget.draw()
class _ConnectionWindow(_Window, BroadcastConnMixin):
self._term.calc_geometry()
for window in self._windows:
window.set_geometry()
- self.window.draw()
elif isinstance(event, _LogEvent):
self.window.log.append(event.payload)
- self.window.log.draw()
elif isinstance(event, _TuiCmdEvent):
cmd = self._cmd_name_to_cmd(event.payload[0])
assert cmd is not None
nick=event.payload)
elif isinstance(event, DisconnectedEvent):
conn_win.prompt.update_prompt(nick_confirmed=False)
- if conn_win == self.window:
- if isinstance(event, LogConnEvent):
- self.window.log.draw()
- if isinstance(event, (NickSetEvent, DisconnectedEvent)):
- self.window.prompt.draw()
else:
return True
+ self.window.draw_tainted()
self._term.flush()
return True