Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiline text tile family #21

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 114 additions & 17 deletions dashing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@

__version__ = "0.1.0"

import abc
import contextlib
import itertools
from collections import deque, namedtuple
from typing import Literal, Optional, Tuple
from enum import IntEnum
from typing import Iterable, Literal, Optional, Sequence, Tuple

from blessed import Terminal

Expand All @@ -87,6 +89,16 @@
Colormap = Tuple[Tuple[float, Color], ...]


class Side(IntEnum):
Left = 0
Right = 1


class Clip(IntEnum):
Head = 0
Tail = 1


class Tile(object):
def __init__(self, title: str = None, border_color: Color = None, color: Color = 0):
self.title = title
Expand Down Expand Up @@ -219,53 +231,138 @@ class HSplit(Split):
pass


class Text(Tile):
class _Multiline(Tile):
"""
A multi-line text box. Example::

Text('Hello World, this is dashing.', border_color=2),

An abstract multi-line text box. Requires to be subclassed.
"""

def __init__(self, text: str, color: Color = 0, **kw):
super().__init__(**kw)
self.text: str = text
self.color = color

@abc.abstractmethod
def _lines(self, width: int, height: int) -> Iterable[str]:
"""
Return max `height` lines to be rendered in the multiline tile.
"""
pass

def _display(self, tbox: TBox, parent: Optional[Tile]):
tbox = self._draw_borders_and_title(tbox)
for dx, line in pad(self.text.splitlines()[-(tbox.h) :], tbox.h):
for dx, line in pad(self._lines(tbox.w, tbox.h), tbox.h):
if dx >= tbox.h:
break
if len(line) >= tbox.w:
line = line[: tbox.w - 1] + "…"
print(
tbox.t.color(self.color)
+ tbox.t.move(tbox.x + dx, tbox.y)
+ line
+ line[: tbox.w]
+ " " * (tbox.w - len(line))
)


class Log(Tile):
class Text(_Multiline):
"""
A multi-line text box. Example::

Text('Hello World, this is dashing.', border_color=2),

"""

def __init__(self, text: str, clip=Clip.Head, **kw):
super().__init__(**kw)
self.text = text
self.clip = clip

@property
def text(self):
return self._text

@text.setter
def text(self, value: str):
self._text = value
self._textlines = value.splitlines()

def _lines(self, width, height: int):
yield from (
self._textlines if self.clip == Clip.Head else self._textlines[-height:]
)


class Log(_Multiline):
"""A log pane that scrolls automatically.
Add new lines with :meth:`append`
"""

def __init__(self, **kw):
def __init__(self, clip=Clip.Tail, **kw):
super().__init__(**kw)
self.logs = deque(maxlen=50)
self.clip = clip

def _display(self, tbox: TBox, parent: Optional[Tile]):
tbox = self._draw_borders_and_title(tbox)
def _lines(self, width: int, height: int):
n_logs = len(self.logs)
log_range = min(n_logs, tbox.h)
log_range = min(n_logs, height)
start = n_logs - log_range
print(tbox.t.color(self.color))
for dx, line in pad((self.logs[ln] for ln in range(start, n_logs)), tbox.h):
print(tbox.t.move(tbox.x + dx, tbox.y) + line + " " * (tbox.w - len(line)))
if self.clip == Clip.Tail:
clipped_range = range(start, n_logs)
else:
clipped_range = range(n_logs, start, -1)
yield from (self.logs[ln] for ln in clipped_range)

def append(self, msg: str):
"""Append a new log message at the bottom"""
self.logs.append(msg)


class DoubleColumn(_Multiline):
"""
A list with a label column and value column
"""

def __init__(
self,
lines: Sequence[Tuple[str, str]],
sep=": ",
trunc_col=Side.Left,
trunc=Side.Right,
clip=Clip.Head,
**kw,
):
super().__init__(**kw)
self.lines = lines
self.sep = sep
self.trunc = trunc
self.trunc_col = trunc_col
self.clip = clip

def _lines(self, width: int, height: int):
if self.clip == Clip.Head:
lines = self.lines[:height]
else:
lines = self.lines[-height:]
size_col = not self.trunc_col
size = min(
width - 1 - len(self.sep),
max((len(cols[size_col]) for cols in lines), default=1),
)
trunc_size = width - size
for cols in lines:
cols = [*cols]
trunc_text = cols[self.trunc_col]
size_text = cols[size_col]
if len(trunc_text) > trunc_size:
if self.trunc == Side.Left:
trunc_text = "…" + trunc_text[-trunc_size + 1 :]
else:
trunc_text = trunc_text[: trunc_size - 1] + "…"
cols[self.trunc_col] = trunc_text
if len(size_text) > size:
cols[size_col] = size_text[: size - 1] + "…"

yield f"{cols[0]}{self.sep}{cols[1]}"


class HGauge(Tile):
"""Horizontal gauge"""

Expand Down