x: int
-class _Widget:
+class _Widget(ABC):
+ _tainted: bool = True
+ _sizes = _YX(-1, -1)
- def __init__(self, **kwargs) -> None:
- super().__init__(**kwargs)
- self._tainted = True
- self._drawable = False
+ @property
+ def _drawable(self) -> bool:
+ return len([m for m in self._sizes if m < 1]) == 0
def taint(self) -> None:
'Declare as in need of re-drawing.'
'If in need of re-drawing.'
return self._tainted
- def set_geometry(self, measurements: _YX) -> bool:
- 'Update widget\'s measurements, re-generate content where necessary.'
- self._tainted = True
- self._drawable = len([m for m in measurements if m < 0]) == 0
- return self._drawable
+ def set_geometry(self, sizes: _YX) -> None:
+ 'Update widget\'s sizues, re-generate content where necessary.'
+ self.taint()
+ self._sizes = sizes
- def draw(self) -> bool:
+ def draw(self) -> None:
'Print widget\'s content in shape appropriate to set geometry.'
- if not self._drawable:
- return False
- self._tainted = False
- return True
+ if self._drawable:
+ self._draw()
+ self._tainted = False
+ @abstractmethod
+ def _draw(self) -> None:
+ pass
-class _ScrollableWidget(_Widget, ABC):
+
+class _ScrollableWidget(_Widget):
_history_idx: int
def __init__(self, write: Callable[..., None], **kwargs) -> None:
@abstractmethod
def _scroll(self, up=True) -> None:
- self._tainted = True
+ self.taint()
def cmd__scroll(self, direction: str) -> None:
'Scroll through stored content/history.'
class _HistoryWidget(_ScrollableWidget):
- _view_size: _YX
_y_pgscroll: int
def __init__(self, wrap: Callable[[str], list[str]], **kwargs) -> None:
self._wrapped += [(idx_original, line) for line in wrapped_lines]
return len(wrapped_lines)
- def set_geometry(self, measurements: _YX) -> bool:
- if not super().set_geometry(measurements):
- return False
- self._view_size = measurements
- self._y_pgscroll = self._view_size.y // 2
- self._wrapped.clear()
- self._wrapped += [(None, '')] * self._view_size.y
- if not self._history:
- return True
- for idx_history, line in enumerate(self._history):
- self._add_wrapped(idx_history, line)
- wrapped_lines_for_history_idx = [
- t for t in self._wrapped
- if t[0] == len(self._history) + self._history_idx]
- idx_their_last = self._wrapped.index(wrapped_lines_for_history_idx[-1])
- self._wrapped_idx = idx_their_last - len(self._wrapped)
- return True
+ def set_geometry(self, sizes: _YX) -> None:
+ super().set_geometry(sizes)
+ if self._drawable:
+ self._y_pgscroll = self._sizes.y // 2
+ self._wrapped.clear()
+ self._wrapped += [(None, '')] * self._sizes.y
+ if self._history:
+ for idx_history, line in enumerate(self._history):
+ self._add_wrapped(idx_history, line)
+ wrapped_lines_for_history_idx = [
+ t for t in self._wrapped
+ if t[0] == len(self._history) + self._history_idx]
+ idx_their_last = self._wrapped.index(
+ wrapped_lines_for_history_idx[-1])
+ self._wrapped_idx = idx_their_last - len(self._wrapped)
def append(self, to_append: str) -> None:
super().append(to_append)
- self._tainted = True
+ self.taint()
if self._history_idx < -1:
self._history_idx -= 1
- if not self._drawable:
- return
- n_wrapped_lines = self._add_wrapped(len(self._history) - 1, to_append)
- if self._wrapped_idx < -1:
- self._wrapped_idx -= n_wrapped_lines
-
- def draw(self) -> bool:
- if not super().draw():
- return False
- start_idx = self._wrapped_idx - self._view_size.y + 1
+ if self._drawable:
+ n_wrapped_lines = self._add_wrapped(len(self._history)
+ - 1, to_append)
+ if self._wrapped_idx < -1:
+ self._wrapped_idx -= n_wrapped_lines
+
+ def _draw(self) -> None:
+ start_idx = self._wrapped_idx - self._sizes.y + 1
end_idx = self._wrapped_idx
to_write = [t[1] for t in self._wrapped[start_idx:end_idx]]
if self._wrapped_idx < -1:
scroll_info = f'vvv [{(-1) * self._wrapped_idx}] '
- scroll_info += 'v' * (self._view_size.x - len(scroll_info))
+ scroll_info += 'v' * (self._sizes.x - len(scroll_info))
to_write += [scroll_info]
else:
to_write += [self._wrapped[self._wrapped_idx][1]]
for i, line in enumerate(to_write):
self._write(line, i)
- return True
def _scroll(self, up: bool = True) -> None:
super()._scroll(up)
- if not self._drawable:
- return
- if up:
- self._wrapped_idx = max(self._view_size.y + 1 - len(self._wrapped),
- self._wrapped_idx - self._y_pgscroll)
- else:
- self._wrapped_idx = min(-1,
- self._wrapped_idx + self._y_pgscroll)
- history_idx_to_wrapped_idx = self._wrapped[self._wrapped_idx][0]
- if history_idx_to_wrapped_idx is not None:
- self._history_idx = history_idx_to_wrapped_idx - len(self._history)
+ if self._drawable:
+ if up:
+ self._wrapped_idx = max(
+ self._sizes.y + 1 - len(self._wrapped),
+ self._wrapped_idx - self._y_pgscroll)
+ else:
+ self._wrapped_idx = min(
+ -1, self._wrapped_idx + self._y_pgscroll)
+ history_idx_to_wrapped_idx = self._wrapped[self._wrapped_idx][0]
+ if history_idx_to_wrapped_idx is not None:
+ self._history_idx = history_idx_to_wrapped_idx\
+ - len(self._history)
class PromptWidget(_ScrollableWidget):
'Manages/displays keyboard input field.'
- _y: int
- _width: int
_history_idx: int = 0
_input_buffer_unsafe: str
_cursor_x: int
@_input_buffer.setter
def _input_buffer(self, content) -> None:
- self._tainted = True
+ self.taint()
self._input_buffer_unsafe = content
- def set_geometry(self, measurements: _YX) -> bool:
- if not super().set_geometry(measurements):
- return False
- self._y, self._width = measurements
- return True
-
- def draw(self) -> bool:
- if not super().draw():
- return False
+ def _draw(self) -> None:
prefix = self.prefix[:]
content = self._input_buffer
if self._cursor_x == len(self._input_buffer):
content += ' '
- half_width = (self._width - len(prefix)) // 2
+ half_width = (self._sizes.x - len(prefix)) // 2
offset = 0
- if len(prefix) + len(content) > self._width\
+ if len(prefix) + len(content) > self._sizes.x\
and self._cursor_x > half_width:
prefix += _PROMPT_ELL_IN
- offset = min(len(prefix) + len(content) - self._width,
+ offset = min(len(prefix) + len(content) - self._sizes.x,
self._cursor_x - half_width + len(_PROMPT_ELL_IN))
cursor_x_to_write = len(prefix) + self._cursor_x - offset
to_write = f'{prefix}{content[offset:]}'
- if len(to_write) > self._width:
- to_write = (to_write[:self._width - len(_PROMPT_ELL_OUT)]
+ if len(to_write) > self._sizes.x:
+ to_write = (to_write[:self._sizes.x-len(_PROMPT_ELL_OUT)]
+ _PROMPT_ELL_OUT)
- self._write(to_write[:cursor_x_to_write], self._y, padding=False)
+ self._write(to_write[:cursor_x_to_write], self._sizes.y,
+ padding=False)
self._write(to_write[cursor_x_to_write], attribute='reverse',
padding=False)
self._write(to_write[cursor_x_to_write + 1:])
- return True
def _archive_prompt(self) -> None:
self.append(self._input_buffer)
self._cursor_x += 1
else:
return
- self._tainted = True
+ self.taint()
def _reset_buffer(self, content: str) -> None:
self._input_buffer = content
class _StatusLine(_Widget):
- _y: int
- _width: int
def __init__(self, write: Callable, status_title: str, **kwargs) -> None:
super().__init__(**kwargs)
self._write = write
self._status_title = status_title
- def set_geometry(self, measurements: _YX) -> bool:
- if not super().set_geometry(measurements):
- return False
- self._y, self._width = measurements
- return True
-
- def draw(self) -> bool:
- if not super().draw():
- return False
+ def _draw(self) -> None:
title_box = f'{self._status_title}]'
- status_line = title_box + '=' * (self._width - len(title_box))
- self._write(status_line, self._y)
- return True
+ status_line = title_box + '=' * (self._sizes.x - len(title_box))
+ self._write(status_line, self._sizes.y)
-class Window(_Widget):
- 'Widget filling entire screen with sub-widgets like .prompt, .history.'
+class Window:
+ 'Collection of widgets filling entire screen.'
_y_status: int
+ _drawable = False
prompt: PromptWidget
def __init__(self, idx: int, term: 'Terminal', **kwargs) -> None:
self.set_geometry()
def taint(self) -> None:
- super().taint()
self.history.taint()
self._status_line.taint()
self.prompt.taint()
@property
def tainted(self) -> bool:
- return self._tainted or self.history.tainted or self.prompt.tainted
+ return (self._status_line.tainted or self.history.tainted
+ or self.prompt.tainted)
- def set_geometry(self, _=None) -> bool:
- assert _ is None
+ def set_geometry(self) -> None:
+ self._drawable = False
if self._term.size.y < _MIN_HEIGHT or self._term.size.x < _MIN_WIDTH:
- bad_yx = _YX(-1, -1)
- super().set_geometry(bad_yx)
- self.history.set_geometry(bad_yx)
- self.prompt.set_geometry(bad_yx)
- return False
- super().set_geometry(_YX(0, 0))
+ for widget in (self.history, self._status_line, self.prompt):
+ widget.set_geometry(_YX(-1, -1))
+ return
self._y_status = self._term.size.y - 2
self.history.set_geometry(_YX(self._y_status, self._term.size.x))
self._status_line.set_geometry(_YX(self._y_status, self._term.size.x))
self.prompt.set_geometry(_YX(self._term.size.y - 1, self._term.size.x))
- return True
+ self._drawable = True
@property
def _title(self) -> str:
'Window title to display in status line.'
return f'{self.idx}) {self._title}'
- def draw(self) -> bool:
- if not super().draw():
- if self._term.size.x > 0:
- lines = ['']
- for i, c in enumerate('screen too small'):
- if i > 0 and 0 == i % self._term.size.x:
- lines += ['']
- lines[-1] += c
- for y, line in enumerate(lines):
- self._term.write(line, y)
- return False
- for widget in [
- w for w in (self.history, self.prompt, self._status_line)
- if w.tainted]:
- widget.draw()
- return True
+ def draw_tainted(self) -> None:
+ if self._drawable:
+ for widget in [
+ w for w in (self.history, self.prompt, self._status_line)
+ if w.tainted]:
+ widget.draw()
+ elif self._term.size.x > 0:
+ lines = ['']
+ for i, c in enumerate('screen too small'):
+ if i > 0 and 0 == i % self._term.size.x:
+ lines += ['']
+ lines[-1] += c
+ for y, line in enumerate(lines):
+ self._term.write(line, y)
def cmd__paste(self) -> None:
'Write OSC 52 ? sequence to get encoded clipboard paste into stdin.'
self._term.write(f'\033{_OSC52_PREFIX.decode()}?{_PASTE_DELIMITER}',
self._y_status)
- self._tainted = True
+ self.taint()
class TuiEvent(AffectiveEvent):
def redraw_affected(self) -> None:
'On focused window call .draw, then flush screen.'
- self.window.draw()
+ self.window.draw_tainted()
self._term.flush()
def _set_screen(self) -> None:
self._term.calc_geometry()
for window in self._windows:
window.set_geometry()
- self._y_status = self._term.size.y - 2
self.redraw_affected()
@property