diff --git a/CHANGELOG.md b/CHANGELOG.md index 54648fd..18320c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog of pigit +## v1.6.0 (2022-08-22) +- Show file icon in tui mode. +- Use new dependencies -- `plenty`(render). +- Independent render part, which is convenient for maintenance. + ## v1.5.2 (2022-04-27) - Fix parameter typing of Python3.8 - Fix `repo ll` command error, when has invalid repo path. diff --git a/docs/classes.png b/docs/classes.png index d2d69c5..0395ffb 100644 Binary files a/docs/classes.png and b/docs/classes.png differ diff --git a/docs/packages.png b/docs/packages.png index 419a958..b4fca1e 100644 Binary files a/docs/packages.png and b/docs/packages.png differ diff --git a/pigit/cmdparse/parser.py b/pigit/cmdparse/parser.py index 02de54c..9c39c2d 100644 --- a/pigit/cmdparse/parser.py +++ b/pigit/cmdparse/parser.py @@ -21,7 +21,7 @@ _SubParsersAction, ) from shutil import get_terminal_size -from pigit.render.style import Style +from plenty.style import Style if TYPE_CHECKING: diff --git a/pigit/codecounter.py b/pigit/codecounter.py index e6d74ee..d5464bd 100644 --- a/pigit/codecounter.py +++ b/pigit/codecounter.py @@ -7,9 +7,10 @@ import concurrent.futures from shutil import get_terminal_size +from plenty.table import Table +from plenty import get_console + from .common.utils import confirm, get_file_icon, adjudgment_type -from .render.table import Table -from .render import get_console Logger = logging.getLogger(__name__) diff --git a/pigit/config.py b/pigit/config.py index 714aeb9..051f764 100644 --- a/pigit/config.py +++ b/pigit/config.py @@ -4,9 +4,10 @@ import os, re, textwrap, logging from distutils.util import strtobool +from plenty.style import Color + from .common.utils import confirm, traceback_info from .common.singleton import Singleton -from .render.style import Color Logger = logging.getLogger(__name__) diff --git a/pigit/const.py b/pigit/const.py index eea4b95..f0db66c 100644 --- a/pigit/const.py +++ b/pigit/const.py @@ -3,7 +3,7 @@ import os, platform, datetime __project__ = "pigit" -__version__ = "1.5.3-dev" +__version__ = "1.6.0" __url__ = "https://github.com/zlj-zz/pigit.git" __uri__ = __url__ diff --git a/pigit/entry.py b/pigit/entry.py index b95ee48..33222af 100644 --- a/pigit/entry.py +++ b/pigit/entry.py @@ -4,6 +4,8 @@ from typing import Dict, List import os, logging, textwrap +from plenty import get_console + from .log import setup_logging from .config import Config from .cmdparse.parser import command, argument, Namespace @@ -18,7 +20,6 @@ COUNTER_DIR_PATH, IS_FIRST_RUN, ) -from .render import get_console from .common.utils import confirm from .common.func import dynamic_default_attrs, time_it from .gitlib.processor import ShortGitter, GIT_CMDS, get_extra_cmds diff --git a/pigit/gitlib/_cmd_func.py b/pigit/gitlib/_cmd_func.py index 4e20245..11db036 100644 --- a/pigit/gitlib/_cmd_func.py +++ b/pigit/gitlib/_cmd_func.py @@ -14,8 +14,9 @@ from typing import List, Tuple, Union +from plenty import echo + from ..common.utils import exec_cmd -from ..render import echo def add(args: Union[List, Tuple]) -> None: diff --git a/pigit/gitlib/options.py b/pigit/gitlib/options.py index 6377d10..5fd6844 100644 --- a/pigit/gitlib/options.py +++ b/pigit/gitlib/options.py @@ -5,9 +5,16 @@ from pathlib import Path import os, re, textwrap, json -from pigit.common.utils import async_run_cmd, exec_async_tasks, exec_cmd -from pigit.render.str_utils import shorten, byte_str2str -from pigit.render.console import Console +from plenty.str_utils import shorten, byte_str2str +from plenty.console import Console + +from pigit.common.utils import ( + adjudgment_type, + async_run_cmd, + exec_async_tasks, + exec_cmd, + get_file_icon, +) from pigit.gitlib.model import File, Commit, Branch @@ -311,6 +318,7 @@ def load_status( ident: int = 2, plain: bool = False, path: Optional[str] = None, + icon: bool = False, ) -> List[File]: """Get the file tree status of GIT for processing and encapsulation. Args: @@ -344,9 +352,12 @@ def load_status( has_inline_merged_conflicts = change in ["UU", "AA"] display_name = shorten(name, max_width - 3 - ident) + + icon_str = get_file_icon(adjudgment_type(display_name)) if icon else "" + # color full command. display_str = Console.render_str( - f"`{staged_change}`<{'bad' if has_no_staged_change else'right'}>`{unstaged_change}`<{'bad' if unstaged_change!=' ' else'right'}> {display_name}" + f"`{staged_change}`<{'bad' if has_no_staged_change else'right'}>`{unstaged_change}`<{'bad' if unstaged_change!=' ' else'right'}> {icon_str}{display_name}" ) file_ = File( diff --git a/pigit/gitlib/processor.py b/pigit/gitlib/processor.py index 2a2a03f..ebe6e8f 100644 --- a/pigit/gitlib/processor.py +++ b/pigit/gitlib/processor.py @@ -3,10 +3,11 @@ from typing import Callable, Dict, List, Optional, Tuple, Union import os, re, textwrap, random, logging +from plenty import get_console +from plenty.str_utils import shorten + from ..common.utils import exec_cmd, confirm, similar_command, traceback_info from ..common.singleton import Singleton -from ..render import get_console -from ..render.str_utils import shorten from .shortcmds import GIT_CMDS, CommandType Log = logging.getLogger(__name__) diff --git a/pigit/info.py b/pigit/info.py index e108751..74a603d 100644 --- a/pigit/info.py +++ b/pigit/info.py @@ -1,10 +1,11 @@ from typing import Dict, Literal, Optional, Tuple import os, re +from plenty.table import UintTable +from plenty import box + from .const import __version__, __url__ from .gitlib.options import GitOption -from .render.table import UintTable -from .render import box git = GitOption() diff --git a/pigit/interaction.py b/pigit/interaction.py index c12d923..36a5d72 100644 --- a/pigit/interaction.py +++ b/pigit/interaction.py @@ -3,12 +3,13 @@ import os, logging from time import sleep +from plenty import get_console +from plenty.style import Style + from .const import IS_WIN from .tui.event_loop import EventLoop, ExitEventLoop from .tui.screen import Screen from .tui.widgets import SwitchWidget, RowPanelWidget, CmdRunner, ConfirmWidget -from .render import get_console -from .render.style import Style from .gitlib.options import GitOption if TYPE_CHECKING: @@ -69,7 +70,7 @@ class StatusPanel(RowPanelWidget): repo_path, repo_conf = git.get_repo_info() def get_raw_data(self) -> List["File"]: - return git.load_status(self.size[0]) + return git.load_status(self.size[0], icon=True) def process_raw_data(self, raw_data: List[Any]) -> List[str]: return ( diff --git a/pigit/render/__init__.py b/pigit/render/__init__.py deleted file mode 100644 index c0db5e4..0000000 --- a/pigit/render/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding:utf-8 -*- -from typing import TYPE_CHECKING -import os - -# For windows print color. -if os.name == "nt": - os.system("") - - -if TYPE_CHECKING: - from .console import Console - - -_console: "Console" = None - - -def get_console(): - global _console - - if not _console: - from .console import Console - - _console = Console() - - return _console - - -def echo( - *values, sep: str = " ", end: str = "\n", file: str = None, flush: bool = True -): - value_list = [get_console().render_str(str(value)) for value in values] - print(*value_list, sep=sep, end=end, file=file, flush=flush) diff --git a/pigit/render/_loop.py b/pigit/render/_loop.py deleted file mode 100644 index 01c6caf..0000000 --- a/pigit/render/_loop.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Iterable, Tuple, TypeVar - -T = TypeVar("T") - - -def loop_first(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: - """Iterate and generate a tuple with a flag for first value.""" - iter_values = iter(values) - try: - value = next(iter_values) - except StopIteration: - return - yield True, value - for value in iter_values: - yield False, value - - -def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: - """Iterate and generate a tuple with a flag for last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - for value in iter_values: - yield False, previous_value - previous_value = value - yield True, previous_value - - -def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: - """Iterate and generate a tuple with a flag for first and last value.""" - iter_values = iter(values) - try: - previous_value = next(iter_values) - except StopIteration: - return - first = True - for value in iter_values: - yield first, False, previous_value - first = False - previous_value = value - yield first, True, previous_value diff --git a/pigit/render/box.py b/pigit/render/box.py deleted file mode 100644 index 4cc6de5..0000000 --- a/pigit/render/box.py +++ /dev/null @@ -1,432 +0,0 @@ -from typing import Iterable, List, Literal, TYPE_CHECKING - -from ._loop import loop_last - -if TYPE_CHECKING: - from .console import Console - - -class Box: - """Defines characters to render boxes. - - ┌─┬┐ top - │ ││ head - ├─┼┤ head_row - │ ││ mid - ├─┼┤ row - ├─┼┤ foot_row - │ ││ foot - └─┴┘ bottom - - Args: - box (str): Characters making up box. - ascii (bool, optional): True if this box uses ascii characters only. Default is False. - """ - - def __init__(self, box: str, *, ascii: bool = False) -> None: - self._box = box - self.ascii = ascii - line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines() - # top - self.top_left, self.top, self.top_divider, self.top_right, _ = iter(line1) - # head - self.head_left, _, self.head_vertical, self.head_right, _ = iter(line2) - # head_row - ( - self.head_row_left, - self.head_row_horizontal, - self.head_row_cross, - self.head_row_right, - self.head_row_up_cross, - self.head_row_down_cross, - _, - ) = iter(line3) - - # mid - self.mid_left, _, self.mid_vertical, self.mid_right, _ = iter(line4) - # row - self.row_left, self.row_horizontal, self.row_cross, self.row_right, _ = iter( - line5 - ) - # foot_row - ( - self.foot_row_left, - self.foot_row_horizontal, - self.foot_row_cross, - self.foot_row_right, - _, - ) = iter(line6) - # foot - self.foot_left, _, self.foot_vertical, self.foot_right, _ = iter(line7) - # bottom - self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right, _ = iter( - line8 - ) - - def __repr__(self) -> str: - return "Box(...)" - - def __str__(self) -> str: - return self._box - - def substitute(self, console: "Console") -> "Box": - box = self - if console.system == "Windows": - box = WINDOWS_SUBSTITUTIONS.get(box, box) - if not console.encoding.startswith("utf"): - box = ASCII - - return box - - def get_top(self, widths: Iterable[int], merge: bool = False) -> str: - """Get the top of a simple box. - - Args: - widths (List[int]): Widths of columns. - - Returns: - str: A string of box characters. - """ - - parts: List[str] = [] - append = parts.append - append(self.top_left) - for last, width in loop_last(widths): - append(self.top * width) - if not last: - if merge: - append(self.top) - else: - append(self.top_divider) - append(self.top_right) - return "".join(parts) - - def get_row( - self, - widths: Iterable[int], - level: Literal["head", "row", "foot", "mid"] = "row", - edge: bool = True, - cross_level: Literal["mid", "up", "down"] = "mid", - ) -> str: - """Get the top of a simple box. - - Args: - width (List[int]): Widths of columns. - - Returns: - str: A string of box characters. - """ - if level == "head": - left = self.head_row_left - horizontal = self.head_row_horizontal - right = self.head_row_right - if cross_level == "up": - cross = self.head_row_up_cross - elif cross_level == "down": - cross = self.head_row_down_cross - else: - cross = self.head_row_cross - elif level == "row": - left = self.row_left - horizontal = self.row_horizontal - right = self.row_right - cross = self.row_cross - elif level == "mid": - left = self.mid_left - horizontal = " " - cross = self.mid_vertical - right = self.mid_right - elif level == "foot": - left = self.foot_row_left - horizontal = self.foot_row_horizontal - cross = self.foot_row_cross - right = self.foot_row_right - else: - raise ValueError("level must be 'head', 'row' or 'foot'") - - parts: List[str] = [] - append = parts.append - if edge: - append(left) - for last, width in loop_last(widths): - append(horizontal * width) - if not last: - append(cross) - if edge: - append(right) - return "".join(parts) - - def get_bottom(self, widths: Iterable[int]) -> str: - """Get the bottom of a simple box. - - Args: - widths (List[int]): Widths of columns. - - Returns: - str: A string of box characters. - """ - - parts: List[str] = [] - append = parts.append - append(self.bottom_left) - for last, width in loop_last(widths): - append(self.bottom * width) - if not last: - append(self.bottom_divider) - append(self.bottom_right) - return "".join(parts) - - -# flake8: noqa -# yapf: disable - -ASCII: Box = Box( - """\ -+--+. -| ||. -|-+|--. -| ||. -|-+|. -|-+|. -| ||. -+--+. -""", - ascii=True, -) - -ASCII2: Box = Box( - """\ -+-++. -| ||. -+-++++. -| ||. -+-++. -+-++. -| ||. -+-++. -""", - ascii=True, -) - -ASCII_DOUBLE_HEAD: Box = Box( - """\ -+-++. -| ||. -+=++++. -| ||. -+-++. -+-++. -| ||. -+-++. -""", - ascii=True, -) - -SQUARE: Box = Box( - """\ -┌─┬┐. -│ ││. -├─┼┤┴┬. -│ ││. -├─┼┤. -├─┼┤. -│ ││. -└─┴┘. -""" -) - -SQUARE_DOUBLE_HEAD: Box = Box( - """\ -┌─┬┐. -│ ││. -╞═╪╡╧╤. -│ ││. -├─┼┤. -├─┼┤. -│ ││. -└─┴┘. -""" -) - -MINIMAL: Box = Box( - """\ - ╷ . - │ . -╶─┼╴┴┬. - │ . -╶─┼╴. -╶─┼╴. - │ . - ╵ . -""" -) - - -MINIMAL_HEAVY_HEAD: Box = Box( - """\ - ╷ . - │ . -╺━┿╸┷┯. - │ . -╶─┼╴. -╶─┼╴. - │ . - ╵ . -""" -) - -MINIMAL_DOUBLE_HEAD: Box = Box( - """\ - ╷ . - │ . - ═╪ ╧╤. - │ . - ─┼ . - ─┼ . - │ . - ╵ . -""" -) - - -SIMPLE: Box = Box( - """\ - . - . - ── ──. - . - . - ── . - . - . -""" -) - -SIMPLE_HEAD: Box = Box( - """\ - . - . - ── ──. - . - . - . - . - . -""" -) - - -SIMPLE_HEAVY: Box = Box( - """\ - . - . - ━━ ━━. - . - . - ━━ . - . - . -""" -) - - -HORIZONTALS: Box = Box( - """\ - ── . - . - ── ──. - . - ── . - ── . - . - ── . -""" -) - -ROUNDED: Box = Box( - """\ -╭─┬╮. -│ ││. -├─┼┤┴┬. -│ ││. -├─┼┤. -├─┼┤. -│ ││. -╰─┴╯. -""" -) - -HEAVY: Box = Box( - """\ -┏━┳┓. -┃ ┃┃. -┣━╋┫┻┳. -┃ ┃┃. -┣━╋┫. -┣━╋┫. -┃ ┃┃. -┗━┻┛. -""" -) - -HEAVY_EDGE: Box = Box( - """\ -┏━┯┓. -┃ │┃. -┠─┼┨┴┬. -┃ │┃. -┠─┼┨. -┠─┼┨. -┃ │┃. -┗━┷┛. -""" -) - -HEAVY_HEAD: Box = Box( - """\ -┏━┳┓. -┃ ┃┃. -┡━╇┩┻┯. -│ ││. -├─┼┤. -├─┼┤. -│ ││. -└─┴┘. -""" -) - -DOUBLE: Box = Box( - """\ -╔═╦╗. -║ ║║. -╠═╬╣╩╦. -║ ║║. -╠═╬╣. -╠═╬╣. -║ ║║. -╚═╩╝. -""" -) - -DOUBLE_EDGE: Box = Box( - """\ -╔═╤╗. -║ │║. -╟─┼╢┴┬. -║ │║. -╟─┼╢. -╟─┼╢. -║ │║. -╚═╧╝. -""" -) - -# Map Boxes that don't render with raster fonts on to equivalent that do -WINDOWS_SUBSTITUTIONS = { - ROUNDED: SQUARE, - MINIMAL_HEAVY_HEAD: MINIMAL, - SIMPLE_HEAVY: SIMPLE, - HEAVY: SQUARE, - HEAVY_EDGE: SQUARE, - HEAVY_HEAD: SQUARE, -} diff --git a/pigit/render/console.py b/pigit/render/console.py deleted file mode 100644 index 2dd3bc1..0000000 --- a/pigit/render/console.py +++ /dev/null @@ -1,138 +0,0 @@ -from typing import Any, Iterable, List, Optional, Union -import sys, platform -from threading import Lock -from itertools import islice -from inspect import isclass -from shutil import get_terminal_size - -from .style import Style -from .markup import render_markup -from .segment import Segment -from .emoji import Emoji -from .errors import NotRenderableError, StyleSyntaxError, MissingStyle - - -class Console: - _lock = Lock() - - def __init__(self) -> None: - self._buffer = [] - self._size = None - - @property - def size(self): - if not self._size: - self._size = get_terminal_size() - - return self._size - - @property - def width(self): - return self.size.columns - - @property - def hight(self): - return self.size.lines - - @property - def system(self) -> str: - """Return the OS name. - - Values: 'Linux', 'Darwin', 'Java', 'Window', '' - """ - - return platform.system() - - @property - def encoding(self): - return sys.getdefaultencoding().lower() - - def get_style( - self, name: Union[str, Style], *, default: Optional[Union[str, Style]] = None - ) -> Style: - if isinstance(name, Style): - return name - - try: - return Style.parse(name) - except StyleSyntaxError as e: - if default is not None: - return self.get_style(default) - raise MissingStyle(f"Failed to get style {name!r}; {e}") from None - - def render_lines( - self, - renderable, - max_width, - *, - style: Optional[Style] = None, - pad: bool = True, - new_lines: bool = False, - ) -> List[Segment]: - _rendered = render_markup(renderable) - if style: - _rendered = Segment.apply_style(_rendered, style) - lines = list( - islice(Segment.split_and_crop_lines(_rendered, max_width), None, None) - ) - return lines - - @classmethod - def render_str( - cls, text: str, /, *, allow_style: bool = True, allow_emoji: bool = True - ) -> str: - """Render color, font and emoji code in string. - - Args: - text (str): The text string that need be rendered. - allow_style (bool, optional): whether render color and font. Defaults to True. - allow_emoji (bool, optional): whether render emoji. Defaults to True. - - Returns: - str: the rendered text string. - """ - - if allow_emoji: - text = Emoji.render_emoji(text) - - if allow_style: - text = Style.render_style(text) - - return text - - def render_str2(self, text: str, /, *, style: Optional[str] = None): - return Segment(text, style) - - def render(self, obj: Any): - render_iterable: Iterable - if isinstance(obj, str): - render_iterable = [self.render_str(obj)] - elif hasattr(obj, "__render__") and not isclass(obj): - render_iterable = obj.__render__(self) - else: - render_iterable = [str(obj)] - - try: - render_iter = iter(render_iterable) - except TypeError: - raise NotRenderableError( - f"object {render_iterable!r} is not renderable" - ) from None - - for render_output in render_iter: - if isinstance(render_output, str): - yield render_output - else: - yield from self.render(render_output) - - def echo(self, *values, sep: str = " ", end: str = "\n", flush: bool = True): - with self._lock: - for value in values: - render_iter = self.render(value) - self._buffer.append("".join(render_iter)) - - # print(self._buffer, len(self._buffer)) - - if self._buffer: - print(*self._buffer, sep=sep, end=end, flush=flush) - del self._buffer[:] diff --git a/pigit/render/emoji.py b/pigit/render/emoji.py deleted file mode 100644 index 9ba72d9..0000000 --- a/pigit/render/emoji.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding:utf-8 -*- -import sys -import re -from typing import Dict, Optional, Match, Callable - -# For encoding. -Icon_Supported_Encoding: list = ["utf-8"] - -_ReStringMatch = Match[str] # regex match object -_ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub -_EmojiSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re -# https://github.com/willmcgugan/rich/blob/master/rich/_emoji_replace.py - -_EMOJI_RE: _ReStringMatch = re.compile( - r"(:(\S*?)(?:(?:\-)(emoji|text))?:)", re.M | re.S -) # allow multi lines. - - -class Emoji(object): - _EMOTION: Dict[str, str] = { - "rainbow": "🌈", - "smiler": "😊", - "thinking": "🧐", - "sorry": "😅", - } - - _WIN_EMOTION: Dict[str, str] = { - "rainbow": "::", - "smiler": "^_^", - "thinking": "-?-", - "sorry": "Orz", - } - - EMOTION: Dict[str, str] - - # XXX(zachary): There are some problems with the output emoji on windows. - # ? In CMD, encoding is right, but emoji is error. - # ? In git bash, encoding is not right, but seem can't detection. - if ( - not sys.platform.lower().startswith("win") - and sys.getdefaultencoding().lower() in Icon_Supported_Encoding - ): - EMOTION = _EMOTION - else: - EMOTION = _WIN_EMOTION - - __locals = locals() - for k, v in EMOTION.items(): - __locals[k] = v - - # Try to render the emoji from str. If the emoji code is invalid will - # keep raw. - # - # +----------------------> content - # | +------> emoji code - # | | - # today is a nice day :rainbow: - @classmethod - def render_emoji( - cls, - _msg: str, - /, - *, - default_variant: Optional[str] = None, - _emoji_sub: _EmojiSubMethod = _EMOJI_RE.sub, - ): - get_emoji = cls.EMOTION.__getitem__ - variants = {"text": "\uFE0E", "emoji": "\uFE0F"} - get_variant = variants.get - default_variant_code = ( - variants.get(default_variant, "") if default_variant else "" - ) - - def do_replace(match: Match[str]) -> str: - emoji_code, emoji_name, variant = match.groups() - try: - return get_emoji(emoji_name.lower()) + get_variant( - variant, default_variant_code - ) - except KeyError: - return emoji_code - - return _emoji_sub(do_replace, _msg) - - -if __name__ == "__main__": - print(Emoji.render_emoji("Today is a nice day :smiler:.")) diff --git a/pigit/render/errors.py b/pigit/render/errors.py deleted file mode 100644 index b33acda..0000000 --- a/pigit/render/errors.py +++ /dev/null @@ -1,22 +0,0 @@ -class NotRenderableError(Exception): - """Object is not renderable.""" - - pass - - -class ColorError(Exception): - """The error of ``Color`` class.""" - - pass - - -class StyleSyntaxError(Exception): - """Style was badly formatted.""" - - pass - - -class MissingStyle(Exception): - """No such style.""" - - pass diff --git a/pigit/render/markup.py b/pigit/render/markup.py deleted file mode 100644 index 87ccfbf..0000000 --- a/pigit/render/markup.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List, Tuple, Union - -from .emoji import Emoji -from .style import _STYLE_RE, Style -from .segment import Segment - - -def _parse(markup: str) -> Tuple: - position = 0 - - for match in _STYLE_RE.finditer(markup): - full_text, fx, pure_text, color, bg_color = match.groups() - start, end = match.span() - if start > position: - yield markup[position:start], None, None, None - if fx or color or bg_color: - yield pure_text, fx, color, bg_color - position = end - - if position < len(markup): - yield markup[position:], None, None, None - - -def render_markup( - markup: str, style: Union[str, Style] = "", emoji: bool = True -) -> List[Segment]: - if emoji: - markup = Emoji.render_emoji(markup) - - renderables: List[Segment] = [] - for text, fx, color, bg_color in _parse(markup): - sgr = [] - if fx: - sgr.append(fx) - if color: - sgr.append(color) - if bg_color: - sgr.extend(("on", bg_color)) - - renderables.append(Segment(text, style=Style.parse(" ".join(sgr)))) - - return renderables diff --git a/pigit/render/ratio.py b/pigit/render/ratio.py deleted file mode 100644 index 29ab49e..0000000 --- a/pigit/render/ratio.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import List - - -def ratio_reduce( - total: int, ratios: List[int], maximums: List[int], values: List[int] -) -> List[int]: - """Divide an integer total in to parts based on ratios. - - Args: - total (int): The total to divide. - ratios (List[int]): A list of integer ratios. - maximums (List[int]): List of maximums values for each slot. - values (List[int]): List of values - - Returns: - List[int]: A list of integers guaranteed to sum to total. - """ - ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)] - total_ratio = sum(ratios) - if not total_ratio: - return values[:] - total_remaining = total - result: List[int] = [] - append = result.append - for ratio, maximum, value in zip(ratios, maximums, values): - if ratio and total_ratio > 0: - distributed = min(maximum, round(ratio * total_remaining / total_ratio)) - append(value - distributed) - total_remaining -= distributed - total_ratio -= ratio - else: - append(value) - return result diff --git a/pigit/render/segment.py b/pigit/render/segment.py deleted file mode 100644 index c927b3c..0000000 --- a/pigit/render/segment.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import TYPE_CHECKING, Iterable, List, Optional, Generator - -from .str_utils import cell_len, set_cell_size, chop_cells -from .style import Style, Fx - - -if TYPE_CHECKING: - from .console import Console - - -class Segment: - def __init__(self, text: str = "", style: Optional[Style] = None) -> None: - self.text = text - self.style = style - self._length = len(text) - - def __render__(self, console: "Console") -> Generator[str, None, None]: - if self.style: - yield self.style.render(self.text) - else: - yield self.text - - def __len__(self): - return self._length - - def __bool__(self): - return bool(self._length) - - def __repr__(self) -> str: - return f"" - - @property - def cell_len(self): - return cell_len(Fx.pure(self.text)) - - @property - def cell_len_without_tag(self): - return cell_len(Style.clear_text(self.text)) - - @classmethod - def line(cls) -> "Segment": - """Make a new line segment.""" - return cls("\n") - - @classmethod - def split_and_crop_lines( - cls, - segments: Iterable["Segment"], - length: int, - style: Optional[str] = None, - pad: bool = True, - include_new_lines: bool = False, - ) -> Iterable[List["Segment"]]: - """Split segments in to lines, and crop lines greater than a given length. - - Args: - segments (Iterable[Segment]): An iterable of segments, probably - generated from console.render. - length (int): Desired line length. - style (Style, optional): Style to use for any padding. - pad (bool): Enable padding of lines that are less than `length`. - - Returns: - Iterable[List[Segment]]: An iterable of lines of segments. - """ - line: List[Segment] = [] - append = line.append - - adjust_line_length = cls.adjust_line_length - new_line_segment = cls("\n") - - for segment in segments: - if "\n" in segment.text: - text = segment.text - style = segment.style - while text: - _text, new_line, text = text.partition("\n") - if _text: - append(cls(_text, style)) - if new_line: - cropped_line = adjust_line_length( - line, length, style=style, pad=pad - ) - if include_new_lines: - cropped_line.append(new_line_segment) - yield cropped_line - del line[:] - else: - append(segment) - if line: - yield adjust_line_length(line, length, style=style, pad=pad) - - @classmethod - def adjust_line_length( - cls, - line: List["Segment"], - length: int, - style: Optional[str] = None, - pad: bool = True, - ) -> List["Segment"]: - """Adjust a line to a given width (cropping or padding as required). - - Args: - segments (Iterable[Segment]): A list of segments in a single line. - length (int): The desired width of the line. - style (Style, optional): The style of padding if used (space on the end). Defaults to None. - pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True. - - Returns: - List[Segment]: A line of segments with the desired length. - """ - line_length = sum(segment.cell_len for segment in line) - new_line: List[Segment] - - if line_length < length: - if pad: - new_line = line + [cls(" " * (length - line_length), style)] - else: - new_line = line[:] - elif line_length > length: - new_line = [] - append = new_line.append - line_length = 0 - for segment in line: - segment_length = segment.cell_len - if line_length + segment_length < length: - append(segment) - line_length += segment_length - else: - text = segment.text - text = set_cell_size(text, length - line_length) - append(cls(text, segment.style)) - break - else: - new_line = line[:] - return new_line - - @classmethod - def apply_style(cls, segments: Iterable["Segment"], style: Optional[Style] = None): - result_segments = segments - if style: - apply = style.__add__ - result_segments = ( - cls(segment.text, apply(segment.style)) for segment in result_segments - ) - return result_segments diff --git a/pigit/render/str_utils.py b/pigit/render/str_utils.py deleted file mode 100644 index 747cec0..0000000 --- a/pigit/render/str_utils.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding:utf-8 -*- - -"""This file support some str util method. - -Docs Test - - The width occupied by a given character when displayed. - - >>> get_width(ord('a')) - 1 - >>> get_width(ord('中')) - 2 - >>> get_width(ord('Ç')) - 1 - - Intercepts a string by a given length. - - >>> shorten('Hello world!', 9, placeholder='^-^') - 'Hello ^-^' - >>> shorten('Hello world!', 9, placeholder='^-^', front=True) - '^-^world!' - - Chop cell. - >>> chop_cells('12345678', 4) - ['1234', '5678'] - >>> chop_cells('12345678', 10) - ['12345678'] - - Set cell size - >>> set_cell_size('123456', 4) - '1234' - >>> set_cell_size('123456', 6) - '123456' - >>> set_cell_size('123456', 8) - '123456 ' -""" - -from typing import List, Tuple -from functools import lru_cache - - -WIDTHS: List[Tuple[int, int]] = [ - (126, 1), - (159, 0), - (687, 1), - (710, 0), - (711, 1), - (727, 0), - (733, 1), - (879, 0), - (1154, 1), - (1161, 0), - (4347, 1), - (4447, 2), - (7467, 1), - (7521, 0), - (8369, 1), - (8426, 0), - (9000, 1), - (9002, 2), - (11021, 1), - (12350, 2), - (12351, 1), - (12438, 2), - (12442, 0), - (19893, 2), - (19967, 1), - (55203, 2), - (63743, 1), - (64106, 2), - (65039, 1), - (65059, 0), - (65131, 2), - (65279, 1), - (65376, 2), - (65500, 1), - (65510, 2), - (120831, 1), - (262141, 2), - (1114109, 1), -] - - -@lru_cache(maxsize=1024) -def get_width(r: int) -> int: - """Gets the width occupied by characters on the command line.""" - - if r in {0xE, 0xF}: - return 0 - return next((wid for num, wid in WIDTHS if r <= num), 1) - - -def get_char_width(character: str) -> int: - return get_width(ord(character)) - - -def cell_len(cell: str) -> int: - return sum(get_char_width(ch) for ch in cell) - - -# TODO: This is inefficient -# TODO: This might not work with CWJ type characters -def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]: - """Break text in to equal (cell) length strings.""" - _get_character_cell_size = get_char_width - characters = [ - (character, _get_character_cell_size(character)) for character in text - ][::-1] - total_size = position - lines: List[List[str]] = [[]] - append = lines[-1].append - - pop = characters.pop - while characters: - character, size = pop() - if total_size + size > max_size: - lines.append([character]) - append = lines[-1].append - total_size = size - else: - total_size += size - append(character) - return ["".join(line) for line in lines] - - -def set_cell_size(text: str, total: int) -> str: - """Set the length of a string to fit within given number of cells.""" - - cell_size = cell_len(text) - if cell_size == total: - return text - if cell_size < total: - return text + " " * (total - cell_size) - - start = 0 - end = len(text) - - # Binary search until we find the right size - while True: - pos = (start + end) // 2 - before = text[: pos + 1] - before_len = cell_len(before) - if before_len == total + 1 and cell_len(before[-1]) == 2: - return f"{before[:-1]} " - if before_len == total: - return before - if before_len > total: - end = pos - else: - start = pos - - -def wrap_color_str(line: str, width: int) -> List[str]: - """Warp a colored line. - Wrap a colored string according to the width of the restriction. - Args: - line: A colored string. - width: Limit width. - """ - # line = re.sub(r"\x1b(?P\[\d+;*\d*[suABCDf])", "\g", line) - # line = line.replace("\\", "\\\\") - line_len = len(line) - lines = [] - start = 0 - i = 0 - count = 0 - while i < line_len: - if line[i] == "\x1b": - while line[i] not in ["m"]: - i += 1 - i += 1 - count += get_char_width(line[i]) if i < line_len else 0 - if count + 1 >= width - 1: - i += 1 - lines.append(line[start:i]) - start = i - count = 0 - if start < line_len: - lines.append(line[start:]) - - return lines - - -def shorten( - text: str, width: int, placeholder: str = "...", front: bool = False -) -> str: - """Truncate exceeded characters. - - Args: - text (str): Target string. - width (int): Limit length. - placeholder (str): Placeholder string. Defaults to "..." - front (bool): Head hidden or tail hidden. Defaults to False. - - Returns: - (str): shorten string. - """ - - if len(text) > width: - if front: - text = placeholder + text[-width + len(placeholder) :] - else: - text = text[: width - len(placeholder)] + placeholder - - return text - - -def byte_str2str(text: str) -> str: - temp = f"b'{text}'" - # use ~eval transfrom `str` to `bytes` - b = eval(temp) - return str(b, encoding="utf-8") - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) - - for line in chop_cells("""窗前明月光,疑似地上霜。举头望明月,低头是故乡。 静夜思 """, 12): - print(line) diff --git a/pigit/render/style.py b/pigit/render/style.py deleted file mode 100644 index b259f0c..0000000 --- a/pigit/render/style.py +++ /dev/null @@ -1,672 +0,0 @@ -# -*- coding:utf-8 -*- - -from typing import List, Literal, Optional, Tuple, Union, Match -import sys, re - -from .errors import ColorError, StyleSyntaxError - - -COLOR_CODE = { - "nocolor": "", - "light_pink": "#FFB6C1", - "pink": "#FFC0CB", - "crimson": "#DC143C", - "lavender_blush": "#FFF0F5", - "pale_violet_red": "#DB7093", - "hot_pink": "#FF69B4", - "deep_pink": "#FF1493", - "medium_violet_red": "#C71585", - "orchid": "#DA70D6", - "thistle": "#D8BFD8", - "plum": "#DDA0DD", - "violet": "#EE82EE", - "magenta": "#FF00FF", - "fuchsia": "#FF00FF", - "dark_magenta": "#8B008B", - "purple": "#800080", - "medium_orchid": "#BA55D3", - "dark_violet": "#9400D3", - "dark_orchid": "#9932CC", - "indigo": "#4B0082", - "blue_violet": "#8A2BE2", - "medium_purple": "#9370DB", - "medium_slateBlue": "#7B68EE", - "slate_blue": "#6A5ACD", - "dark_slate_blue": "#483D8B", - "lavender": "#E6E6FA", - "ghost_white": "#F8F8FF", - "blue": "#0000FF", - "medium_blue": "#0000CD", - "midnight_blue": "#191970", - "dark_blue": "#00008B", - "navy": "#000080", - "royal_blue": "#4169E1", - "cornflower_blue": "#6495ED", - "light_steel_blue": "#B0C4DE", - "light_slate_gray": "#778899", - "slate_gray": "#708090", - "dodder_blue": "#1E90FF", - "alice_blue": "#F0F8FF", - "steel_blue": "#4682B4", - "light_sky_blue": "#87CEFA", - "sky_blue": "#87CEEB", - "deep_sky_blue": "#00BFFF", - "light_blue": "#ADD8E6", - "powder_blue": "#B0E0E6", - "cadet_blue": "#5F9EA0", - "azure": "#F0FFFF", - "light_cyan": "#E1FFFF", - "pale_turquoise": "#AFEEEE", - "cyan": "#00FFFF", - "aqua": "#D4F2E7", - "dark_turquoise": "#00CED1", - "dark_slate_gray": "#2F4F4F", - "dark_cyan": "#008B8B", - "teal": "#008080", - "medium_turquoise": "#48D1CC", - "light_sea_green": "#20B2AA", - "turquoise": "#40E0D0", - "aquamarine": "#7FFFAA", - "medium_aquamarine": "#00FA9A", - "medium_spring_green": "#00FF7F", - "mint_cream": "#F5FFFA", - "spring_green": "#3CB371", - "sea_green": "#2E8B57", - "honeydew": "#F0FFF0", - "light_green": "#90EE90", - "pale_green": "#98FB98", - "ok": "#98FB98", - "good": "#98FB98", - "right": "#98FB98", - "dark_sea_green": "#8FBC8F", - "lime_green": "#32CD32", - "lime": "#00FF00", - "forest_green": "#228B22", - "green": "#008000", - "dark_green": "#006400", - "chartreuse": "#7FFF00", - "lawn_green": "#7CFC00", - "green_yellow": "#ADFF2F", - "olive_drab": "#556B2F", - "beige": "#F5F5DC", - "light_goldenrod_yellow": "#FAFAD2", - "ivory": "#FFFFF0", - "light_yellow": "#FFFFE0", - "yellow": "#FFFF00", - "olive": "#808000", - "dark_khaki": "#BDB76B", - "lemon_chiffon": "#FFFACD", - "pale_goldenrod": "#EEE8AA", - "khaki": "#F0E68C", - "gold": "#FFD700", - "cornsilk": "#FFF8DC", - "goldenrod": "#DAA520", - "floral_white": "#FFFAF0", - "old_lace": "#FDF5E6", - "wheat": "#F5DEB3", - "moccasin": "#FFE4B5", - "orange": "#FFA500", - "papaya_whip": "#FFEFD5", - "blanched_almond": "#FFEBCD", - "navajo_white": "#FFDEAD", - "antique_white": "#FAEBD7", - "tan": "#D2B48C", - "burly_wood": "#DEB887", - "bisque": "#FFE4C4", - "dark_orange": "#FF8C00", - "linen": "#FAF0E6", - "peru": "#CD853F", - "peach_puff": "#FFDAB9", - "sandy_brown": "#F4A460", - "chocolate": "#D2691E", - "saddle_brown": "#8B4513", - "sea_shell": "#FFF5EE", - "sienna": "#A0522D", - "light_salmon": "#FFA07A", - "coral": "#FF7F50", - "orange_red": "#FF4500", - "dark_salmon": "#E9967A", - "tomato": "#FF6347", - "bad": "#FF6347", - "error": "#FF6347", - "misty_rose": "#FFE4E1", - "salmon": "#FA8072", - "snow": "#FFFAFA", - "light_coral": "#F08080", - "rosy_brown": "#BC8F8F", - "indian_red": "#CD5C5C", - "red": "#FF0000", - "brown": "#A52A2A", - "fire_brick": "#B22222", - "dark_red": "#8B0000", - "maroon": "#800000", - "white": "#FFFFFF", - "white_smoke": "#F5F5F5", - "bright_gray": "#DCDCDC", - "light_grey": "#D3D3D3", - "silver": "#C0C0C0", - "dark_gray": "#A9A9A9", - "gray": "#808080", - "dim_gray": "#696969", - "black": "#000000", -} - -ColorType = Union[str, List, Tuple] -ColorDepthType = Literal["fg", "bg"] - -# color hexa string reg. -_COLOR_RE = re.compile(r"^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{2})$") - -# If has special format string, will try to render the color and font style. -# If cannot to render the string will keep it. -# -# .+-----------------------------------> font style prefix (options). -# | +-------------------------> the content being rendered. -# | | +-----------> color code or color name, like: blue (options). -# | | | +---> background color code. -# | | | | -# | | | | -# b`This is a string.`<#FF0000,#00FF00> -# -# Must keep has one of font style or color for making sure can right render. -# If ignore the two both, it will do nothing. -# Only '`' with consecutive beginning and ending will be considered part of the content. -_STYLE_RE = re.compile( - r"(([a-z]+|\((?:[a-z\s],?)+\))?`(`*.*?`*)`(?:<([a-zA-Z_]+|#[0-9a-fA-F]{6})?(?:,([a-zA-Z_]+|#[0-9a-fA-F]{6}))?>)?)", - re.M | re.S, # allow multi lines. -) - - -class Fx(object): - """Text effects - * trans(string: str): Replace whitespace with escape move right to not - overwrite background behind whitespace. - * uncolor(string: str) : Removes all 24-bit color and returns string . - * pure(string: str): Removes all style string. - - Docs test - >>> txt = '\033[49;1;20m\033[1mhello word!\033[0m' - >>> Fx.pure(txt) - 'hello word!' - - """ - - start = "\033[" # * Escape sequence start - sep = ";" # * Escape sequence separator - end = "m" # * Escape sequence end - # * Reset foreground/background color and text effects - reset = rs = "\033[0m" - bold = b = "\033[1m" # * Bold on - unbold = ub = "\033[22m" # * Bold off - dark = d = "\033[2m" # * Dark on - undark = ud = "\033[22m" # * Dark off - italic = i = "\033[3m" # * Italic on - unitalic = ui = "\033[23m" # * Italic off - underline = u = "\033[4m" # * Underline on - ununderline = uu = "\033[24m" # * Underline off - blink = bl = "\033[5m" # * Blink on - unblink = ubl = "\033[25m" # * Blink off - strike = s = "\033[9m" # * Strike / crossed-out on - unstrike = us = "\033[29m" # * Strike / crossed-out off - - supports = { - "bold": "bold", - "b": "bold", - "dark": "dark", - "d": "dark", - "italic": "italic", - "i": "italic", - "underline": "underline", - "u": "underline", - "strike": "strike", - "s": "strike", - "blink": "blink", - } - - code_map = { - 0: "1", - 1: "2", - 2: "3", - 3: "4", - 4: "5", - 5: "9", - } - - # * Precompiled regex for finding a 24-bit color escape sequence in a string - color_re = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m") - style_re = re.compile(r"\033\[\d+m") - - @staticmethod - def trans(string): - return string.replace(" ", "\033[1C") - - @classmethod - def uncolor(cls, string): - return cls.color_re.sub("", string) - - @classmethod - def pure(cls, string): - return cls.style_re.sub("", cls.uncolor(string)) - - @classmethod - def by_name(cls, name: str) -> str: - try: - fx_code = getattr(cls, name) - except AttributeError: - fx_code = "" - - return fx_code - - -class Color(object): - """Holds representations for a 24-bit color value - - __init__(color, depth="fg", default=False) - : color accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit - hexadecimal: string "#FF" or decimal RGB "255 255 255" as a string. - : depth accepts "fg" or "bg" - __call__(*args) joins str arguments to a string and apply color - __str__ returns escape sequence to set color - __iter__ returns iteration over red, green and blue in integer values of 0-255. - - * Values: - .hexa: str - .dec: Tuple[int, int, int] - .red: int - .green: int - .blue: int - .depth: str - .escape: str - """ - - hexa: str - rgb: Tuple[int, int, int] - red: int - green: int - blue: int - depth: str - escape: str - default: bool - - TRUE_COLOR = sys.version_info < (3, 0) - - def __init__( - self, - color: Optional[ColorType] = None, - depth: ColorDepthType = "fg", - default: bool = False, - ) -> None: - self.depth = depth - self.default = default - - if not color: - self.rgb = (-1, -1, -1) - self.hexa = "" - self.red = self.green = self.blue = -1 - self.escape = "\033[49m" if depth == "bg" and default else "" - return - - if not self.is_color(color): - raise ColorError("Not valid color.") from None - - if isinstance(color, str): - self.rgb = rgb = self.generate_rgb(color) - self.hexa = color - else: # list or tuple - self.rgb = rgb = color - # sourcery skip: replace-interpolation-with-fstring - self.hexa = "#%s%s%s" % ( - hex(rgb[0]).lstrip("0x").zfill(2), - hex(rgb[1]).lstrip("0x").zfill(2), - hex(rgb[2]).lstrip("0x").zfill(2), - ) - - self.escape = self.escape_color(r=rgb[0], g=rgb[1], b=rgb[2], depth=depth) - - def __str__(self): - return self.escape - - def __repr__(self): - return repr(self.escape) - - def __iter__(self): - yield from self.rgb - - @staticmethod - def generate_rgb(hexa: str) -> Tuple: - hexa_len = len(hexa) - try: - if hexa_len == 3: - c = int(hexa[1:], base=16) - rgb = (c, c, c) - elif hexa_len == 7: - rgb = ( - int(hexa[1:3], base=16), - int(hexa[3:5], base=16), - int(hexa[5:7], base=16), - ) - except ValueError: - raise ColorError( - f"The hexa `{hexa}` of color can't to be parsing." - ) from None - else: - return rgb - - @staticmethod - def truecolor_to_256(rgb: Tuple) -> int: - - greyscale = (rgb[0] // 11, rgb[1] // 11, rgb[2] // 11) - if greyscale[0] == greyscale[1] == greyscale[2]: - return 232 + greyscale[0] - else: - return ( - round(rgb[0] / 51) * 36 - + round(rgb[1] / 51) * 6 - + round(rgb[2] / 51) - + 16 - ) - - @classmethod - def escape_color( - cls, - hexa: Optional[str] = None, - r: int = 0, - g: int = 0, - b: int = 0, - depth: ColorDepthType = "fg", - ) -> str: - """Returns escape sequence to set color - - Args: - hexa (str): accepts either 6 digit hexadecimal hexa="#RRGGBB", - 2 digit hexadecimal: hexa="#FF". - r (int): 0-255, the r of decimal RGB. - g (int): 0-255, the g of decimal RGB. - b (int): 0-255, the b of decimal RGB. - - Returns: - color (str): ascii color code. - """ - - dint = 38 if depth == "fg" else 48 - rgb = cls.generate_rgb(hexa) if hexa else (r, g, b) - - if not Color.TRUE_COLOR: - return "\033[{};5;{}m".format(dint, Color.truecolor_to_256(rgb=rgb)) - - return "\033[{};2;{};{};{}m".format(dint, *rgb) - - @classmethod - def fg(cls, *args) -> str: - if len(args) > 2: - return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="fg") - else: - return cls.escape_color(hexa=args[0], depth="fg") - - @classmethod - def bg(cls, *args) -> str: - if len(args) > 2: - return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="bg") - else: - return cls.escape_color(hexa=args[0], depth="bg") - - @classmethod - def by_name(cls, name: str, depth: ColorDepthType = "fg"): - """Get color ascii code by support color name.""" - - color_hexa = COLOR_CODE.get(name, "") - - if not color_hexa: - return color_hexa - - if depth == "fg": - return cls.fg(color_hexa) - elif depth == "bg": - return cls.bg(color_hexa) - else: - return "" - - @staticmethod - def is_color(code: Union[str, List, Tuple]) -> bool: - """Return True if code is color else False. - Like: '#FF0000', '#FF', 'red', [255, 0, 0], (0, 255, 0) - """ - - if type(code) == str: - return ( - _COLOR_RE.match(str(code)) is not None - or COLOR_CODE.get(code) is not None - ) - elif isinstance(code, (list, tuple)): - if len(code) != 3: - return False - return all(0 <= c <= 255 for c in code) - else: - return False - - -class Style(object): - def __init__( - self, - *, - color: Optional[str] = None, - bg_color: Optional[str] = None, - bold: Optional[bool] = None, - dark: Optional[bool] = None, - italic: Optional[bool] = None, - underline: Optional[bool] = None, - blink: Optional[bool] = None, - strick: Optional[bool] = None, - ) -> None: - self.color = color if Color.is_color(color) else None - self.bg_color = bg_color if Color.is_color(bg_color) else None - - self._set_attributes = sum( - ( - bold is not None and 1, - dark is not None and 2, - italic is not None and 4, - underline is not None and 8, - blink is not None and 16, - strick is not None and 32, - ) - ) - self._attributes = sum( - ( - bold and 1 or 0, - dark and 2 or 0, - italic and 4 or 0, - underline and 8 or 0, - blink and 16 or 0, - strick and 32 or 0, - ) - ) - - self._style_definition: Optional[str] = None - self._ansi: Optional[str] = None - self._null = not (self._set_attributes or color or bg_color) - - def __str__(self) -> str: - if self._style_definition is None: - style_res: List[str] = [] - append = style_res.append - - bits = self._set_attributes - bits2 = self._attributes - if bits & 0b000001111: - if bits & 1: - append("bold" if bits2 & 1 else "not bold") - if bits & (1 << 1): - append("dark" if bits2 & (1 << 1) else "not dark") - if bits & (1 << 2): - append("italic" if bits2 & (1 << 2) else "not italic") - if bits & (1 << 3): - append("underline" if bits2 & (1 << 3) else "not underline") - if bits & 0b111110000: - if bits & (1 << 4): - append("blink" if bits2 & (1 << 4) else "not blink") - if bits & (1 << 5): - append("strick" if bits2 & (1 << 5) else "not strick") - - if self.color: - style_res.append(self.color) - if self.bg_color: - style_res.extend(("on", self.bg_color)) - - self._style_definition = " ".join(style_res) or "none" - - return self._style_definition - - def _make_ansi_code(self) -> str: - if self._ansi is None: - sgr: List[str] = [] - fx_map = Fx.code_map - - if attributes := self._set_attributes & self._attributes: - sgr.extend(fx_map[bit] for bit in range(6) if attributes & (1 << bit)) - - self._ansi = f"{Fx.start}{';'.join(sgr)}{Fx.end}" - if self.color: - self._ansi += ( - Color.fg(self.color) - if self.color.startswith("#") - else Color.by_name(self.color) - ) - if self.bg_color: - self._ansi += ( - Color.bg(self.bg_color) - if self.bg_color.startswith("#") - else Color.by_name(self.bg_color, depth="bg") - ) - - # print(repr(self._ansi)) - return self._ansi - - def render(self, text: str) -> str: - attrs = self._make_ansi_code() - return f"{attrs}{text}{Fx.reset}" if attrs else text - - def test(self, text: Optional[str] = None) -> None: - text = text or str(self) - print(self.render(text)) - - def __add__(self, style: Optional["Style"]) -> "Style": - if not (isinstance(style, Style) or Style is None): - return NotImplemented - - if style is None or style._null: - return self - - new_style: Style = self.__new__(Style) - new_style._ansi = None - new_style._style_definition = None - new_style.color = style.color or self.color - new_style.bg_color = style.bg_color or self.bg_color - new_style._attributes = (self._attributes & ~style._set_attributes) | ( - style._attributes & style._set_attributes - ) - new_style._set_attributes = self._set_attributes | style._set_attributes - new_style._null = style._null or self._null - - return new_style - - @classmethod - def parse(cls, style_definition: str) -> "Style": - FX_ATTRIBUTES = Fx.supports - color = "" - bg_color = "" - attributes = {} - - words = iter(style_definition.split()) - for original_word in words: - word = original_word.lower() - - if word == "on": - word = next(words, "") - if not word: - raise StyleSyntaxError("color expected after 'on'") - if Color.is_color(word): - bg_color = word - else: - raise StyleSyntaxError( - f"unable to parse {word!r} as background color." - ) - - elif word in FX_ATTRIBUTES: - attributes[FX_ATTRIBUTES[word]] = True - - elif Color.is_color(word): - color = word - else: - raise StyleSyntaxError(f"unable to parse {word!r} as color.") - - return Style(color=color, bg_color=bg_color, **attributes) - - @staticmethod - def render_style(_msg: str, /, *, _style_sub=_STYLE_RE.sub) -> str: - def do_replace(match: Match[str]) -> str: - raw, fx_tag, content, color_code, bg_color_code = match.groups() - # print(raw, fx_tag, content, color_code, bg_color_code) - - if not color_code and not fx_tag and not bg_color_code: - return raw - - try: - if fx_tag is None: - # No fx then get empty. - font_style = "" - elif fx_tag.startswith("(") and fx_tag.endswith(")"): - # Has multi fx tags. - fx_tag = fx_tag[1:-1] - font_style = "".join( - Fx.by_name(fx_code.strip()) for fx_code in fx_tag.split(",") - ) - else: - # Only one. - font_style = Fx.by_name(fx_tag) - - # Get color hexa. - if color_code and color_code.startswith("#"): - color_style = Color.fg(color_code) - else: - color_style = Color.by_name(color_code, depth="fg") - - if bg_color_code and bg_color_code.startswith("#"): - bg_color_style = Color.bg(bg_color_code) - else: - bg_color_style = Color.by_name(bg_color_code, depth="bg") - - return f"{font_style}{color_style}{bg_color_style}{content}\033[0m" - except KeyError: - return raw - - return _style_sub(do_replace, _msg) - - @classmethod - def remove_style(cls, _msg: str, /, *, _style_sub=_STYLE_RE.sub) -> str: - def do_replace(match: Match[str]) -> str: - raw, fx, content, color_code, color_bg_code = match.groups() - - if not color_code and not fx and not color_bg_code: - return raw - - return content - - return _style_sub(do_replace, _msg) - - @classmethod - def clear_text(cls, _msg: str) -> str: - return Fx.pure(cls.remove_style(_msg)) - - @classmethod - def null(cls) -> "Style": - return NULL_STYLE - - -NULL_STYLE = Style() - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) diff --git a/pigit/render/table.py b/pigit/render/table.py deleted file mode 100644 index 9d8b6f7..0000000 --- a/pigit/render/table.py +++ /dev/null @@ -1,652 +0,0 @@ -# -*- coding:utf-8 -*- - -from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple, Union -from dataclasses import dataclass, field - -# from . import box -from .box import Box, HEAVY_HEAD -from .style import Style -from .segment import Segment -from .ratio import ratio_reduce -from ._loop import loop_first_last, loop_last - -if TYPE_CHECKING: - from .console import Console - - -@dataclass -class BaseTb: - """A base class of all table class.""" - - title: Optional[str] = "" - caption: Optional[str] = "" - box: Box = HEAVY_HEAD - width: Optional[int] = None - show_edge: bool = True - show_lines: bool = False - show_header: bool = True - title_style: Optional[str] = None - caption_style: Optional[str] = None - border_style: Optional[str] = None - - def _collapse_widths( - self, widths: List[int], wrapable: List[bool], max_width: int - ) -> List[int]: - """Reduce widths so that the total is under max_width. - - Args: - widths (List[int]): List of widths. - wrapable (List[bool]): List of booleans that indicate if a column may shrink. - max_width (int): Maximum width to reduce to. - - Returns: - List[int]: A new list of widths. - """ - - total_width = sum(widths) - excess_width = total_width - max_width - - if any(wrapable): - while total_width and excess_width > 0: - max_column = max( - width for width, allow_wrap in zip(widths, wrapable) if allow_wrap - ) - second_max_column = max( - width if allow_wrap and width != max_column else 0 - for width, allow_wrap in zip(widths, wrapable) - ) - column_difference = max_column - second_max_column - - ratios = [ - (1 if (width == max_column and allow_wrap) else 0) - for width, allow_wrap in zip(widths, wrapable) - ] - if not any(ratios) or not column_difference: - break - max_reduce = [min(excess_width, column_difference)] * len(widths) - widths = ratio_reduce(excess_width, ratios, max_reduce, widths) - - total_width = sum(widths) - excess_width = total_width - max_width - - return widths - - def set_shape(self, height: int, lines: List, width: int) -> List[Segment]: - extra_lines = height - len(lines) - blank = [Segment(" " * width)] - shaped_lines = lines[:height] - if extra_lines: - shaped_lines = lines + [blank * extra_lines] - - return shaped_lines - - -@dataclass -class Column(object): - _index: int = 0 - - header: str = "" - - header_style: Union[str, Style] = "" - - style: Union[str, Style] = "" - - no_wrap: bool = False - - _cells: List = field(default_factory=list) - - -@dataclass -class Row: - style: Union[str, Style] = "" - - end_section: bool = False - - -@dataclass -class Table(BaseTb): - def __post_init__(self): - self._columns: List[Column] = [] - self._rows: List[Row] = [] - - @property - def _extra_width(self) -> int: - width = 0 - if self.box and self.show_edge: - width += 2 - if self.box: - width += len(self._columns) - 1 - - return width - - def add_column( - self, - header, - header_style: Optional[str] = "bold", - style: Optional[str] = None, - no_wrap: bool = False, - ) -> None: - column = Column( - header=header, - header_style=header_style, - style=style, - no_wrap=no_wrap, - _index=len(self._columns), - ) - - self._columns.append(column) - - def add_row( - self, *values, style: Optional[str] = None, end_section: bool = False - ) -> None: - cells = values - columns = self._columns - - if len(values) < len(columns): - cells = [*cells, *[None] * len(columns) - len(values)] - - for idx, cell in enumerate(cells): - if idx == len(columns): - column = Column(_index=idx) - for _ in self._rows: - column._cells.append("") - columns.append(column) - else: - column = columns[idx] - - if cell is None: - column._cells.append("") - else: - column._cells.append(cell) - - self._rows.append(Row(style=style, end_section=end_section)) - - def get_row_style(self, console: "Console", index: int) -> Style: - """Get current row style.""" - - style = Style.null() - row_style = self._rows[index].style - if row_style is not None: - style += console.get_style(row_style) - - return style - - def _get_cells(self, console: "Console", column: Column) -> List[Segment]: - raw_cells = [] - - if self.show_header: - raw_cells.append( - Segment(column.header, console.get_style(column.header_style or "")) - ) - - cell_style = console.get_style(column.style or "") - raw_cells.extend(Segment(cell, cell_style) for cell in column._cells) - return raw_cells - - def _measure_column( - self, console: "Console", column: Column, max_width: int - ) -> int: - if max_width < 1: - return 0 - - cells = self._get_cells(console, column) - - return max(cell.cell_len_without_tag for cell in cells) - - def _calc_column_widths(self, console: "Console", max_width: int) -> List[int]: - columns = self._columns - - widths = [ - self._measure_column(console, column, max_width) for column in columns - ] - - table_width = sum(widths) - - if table_width > max_width: - widths = self._collapse_widths( - widths, [not column.no_wrap for column in columns], max_width - ) - table_width = sum(widths) - if table_width > max_width: - excess_width = table_width - max_width - widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths) - # table_width = sum(widths) - - return widths - - def _render(self, console: "Console", widths: List[int]) -> Generator: - - border_style = console.get_style(self.border_style or "") - _columns_cells = [self._get_cells(console, column) for column in self._columns] - row_cells = list(zip(*_columns_cells)) - - columns = self._columns - show_edge = self.show_edge - show_lines = self.show_lines - show_header = self.show_header - - _box = self.box.substitute(console) if self.box else None - new_line = Segment.line() - - if _box: - box_segments = [ - ( - Segment(_box.head_left, border_style), - Segment(_box.head_right, border_style), - Segment(_box.head_vertical, border_style), - ), - ( - Segment(_box.foot_left, border_style), - Segment(_box.foot_right, border_style), - Segment(_box.foot_vertical, border_style), - ), - ( - Segment(_box.mid_left, border_style), - Segment(_box.mid_right, border_style), - Segment(_box.mid_vertical, border_style), - ), - ] - if show_edge: - yield Segment(_box.get_top(widths), border_style) - yield new_line - else: - box_segments = [] - - set_shape = self.set_shape - get_row_style = self.get_row_style - get_style = console.get_style - - for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)): - header_row = first and show_header - footer_row = last - row = ( - self._rows[index - show_header] - if (not header_row and not footer_row) - else None - ) - - max_height = 1 - cells: List = [] - if header_row or footer_row: - row_style = Style.null() - else: - row_style = get_style( - get_row_style(console, index - 1 if show_header else index) - ) - - for width, cell, column in zip(widths, row_cell, columns): - lines = console.render_lines( - cell.text, width, style=cell.style + row_style - ) - max_height = max(max_height, len(lines)) - cells.append(lines) - - row_height = max(len(cell) for cell in cells) - - cells[:] = [ - set_shape(max_height, cell, width) for width, cell in zip(widths, cells) - ] - - if _box: - left, right, _divider = box_segments[0 if first else (2 if last else 1)] - - # If the column divider is whitespace also style it with the row background - divider = _divider - for line_no in range(max_height): - if show_edge: - yield left - for last_cell, rendered_cell in loop_last(cells): - yield from rendered_cell[line_no] - if not last_cell: - yield divider - if show_edge: - yield right - yield new_line - else: - for line_no in range(max_height): - for rendered_cell in cells: - yield from rendered_cell[line_no] - yield new_line - if _box and first and show_header: - yield Segment( - _box.get_row(widths, "head", edge=show_edge), style=border_style - ) - yield new_line - end_section = row and row.end_section - if _box and (show_lines or end_section): - if ( - not last - # and not (show_footer and index >= len(row_cells) - 2) - and not (show_header and header_row) - ): - # yield Segment( - # _box.get_row(widths, "mid", edge=show_edge), style=border_style - # ) - yield Segment( - _box.get_row(widths, "row", edge=show_edge), style=border_style - ) - yield new_line - - if _box and show_edge: - yield Segment(_box.get_bottom(widths), style=border_style) - yield new_line - - def __render__(self, console: "Console") -> Generator: - - if not self._columns: - yield Segment("\n") - return - - max_width = console.width - if self.width is not None: - max_width = self.width - - extra_width = self._extra_width - widths = self._calc_column_widths(console, max_width - extra_width) - table_width = sum(widths) + extra_width - - def render_annotation(text, style): - render_text = ( - console.render_str2(text, style=style) - if isinstance(text, str) - else text - ) - return render_text - - if self.title: - yield render_annotation( - (table_width - len(self.title)) // 2 * " " + self.title + "\n", - style=console.get_style(self.title_style or ""), - ) - yield from self._render(console, widths) - if self.caption: - yield render_annotation( - (table_width - len(self.caption)) // 2 * " " + self.caption + "\n", - style=console.get_style(self.caption_style or ""), - ) - - -@dataclass -class Unit(object): - _index: int = 0 - header: str = "" - header_style: Union[str, Style] = "" - kv: Dict = field(default_factory=dict) - kv_style: Union[None, str, Style, List[Union[str, Style]]] = None - - def __post_init__(self): - style = self.kv_style - if style is None: - self.kv_style = ["", ""] - elif isinstance(style, (str, Style)): - self.kv_style = [style, ""] - elif isinstance(style, list) and len(style) < 2: - self.kv_style.append("") - - def add_kv(self, key, value, cover: bool = False) -> bool: - # sourcery skip: merge-duplicate-blocks - if cover: - self.kv[key] = value - - elif self.kv.get(key) is None: - self.kv[key] = value - - else: - return False - - return True - - -@dataclass -class UintTable(BaseTb): - """ - - table format: - ┏━━━━━━━━━━━━━┓ - ┃ Fruit color ┃ - ┣━━━━━━┳━━━━━━┫ - ┃apple ┃red ┃ - ┃grape ┃purple┃ - ┣━━━━━━┻━━━━━━┫ - ┃Animal color ┃ - ┣━━━━━━┳━━━━━━┫ - ┃cattle┃yellow┃ - ┃sheep ┃white ┃ - ┣━━━━━━┻━━━━━━┫ - ┃ ────END┃ - ┗━━━━━━━━━━━━━┛ - """ - - def __post_init__(self) -> None: - self.units: List[Unit] = [] - - @property - def _extra_width(self) -> int: - width = 0 - if self.box and self.show_edge: - width += 2 - if self.box: - width += 1 - - return width - - def add_unit( - self, - header: str, - header_style: Union[str, Style] = None, - values: Optional[Dict] = None, - values_style: Union[None, str, Style, List[Union[str, Style]]] = None, - ) -> Unit: - if values is None: - values = {} - - unit = Unit( - header=header, - header_style=header_style, - kv=values, - kv_style=values_style, - _index=len(self.units), - ) - self.units.append(unit) - return unit - - def _measure_unit( - self, console: "Console", unit: Unit, max_width: int - ) -> Tuple[int, int]: - # TODO: process for chinese - if max_width < 1: - return (0, 0) - - header_len = len(unit.header) - col1 = max(len(key) for key in unit.kv) - col2 = max(len(value) for value in unit.kv.values()) - - if header_len > col1 + col2: - while True: - col2 += 1 - if header_len <= col1 + col2: - break - col1 += 1 - if header_len <= col1 + col2: - break - - return (col1, col2) - - def _calc_unit_widths(self, console: "Console", max_width: int): - units = self.units - - width_range = [self._measure_unit(console, unit, max_width) for unit in units] - col1 = col2 = 0 - for item in width_range: - col1 = max(col1, item[0]) - col2 = max(col2, item[1]) - - widths = [col1, col2] - table_width = col1 + col2 - - if table_width > max_width: - widths = self._collapse_widths(widths, [True, True], max_width) - table_width = sum(widths) - if table_width > max_width: - excess_width = table_width - max_width - widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths) - # table_width = sum(widths) - - return widths - - def _render_line( - self, cells, height, box_segments, show_edge, is_header: bool = False - ) -> Generator: - new_line = Segment.line() - - if box_segments: - left, right, _divider = box_segments[ - # 0 if first else (2 if last else 1) - 0 - if is_header - else 1 - ] - - # If the column divider is whitespace also style it with the row background - divider = _divider - for line_no in range(height): - if show_edge: - yield left - for last_cell, rendered_cell in loop_last(cells): - yield from rendered_cell[line_no] - if not last_cell: - yield divider - if show_edge: - yield right - yield new_line - else: - for line_no in range(height): - for rendered_cell in cells: - yield from rendered_cell[line_no] - yield new_line - - def _render(self, console: "Console", widths: List[int]) -> Generator: - border_style = console.get_style(self.border_style or "") - units = self.units - # print(units) - - show_edge = self.show_edge - show_lines = self.show_lines - show_header = self.show_header - - _box = self.box.substitute(console) if self.box else None - new_line = Segment.line() - - if _box: - box_segments = [ - ( - Segment(_box.head_left, border_style), - Segment(_box.head_right, border_style), - Segment(_box.head_vertical, border_style), - ), - ( - Segment(_box.foot_left, border_style), - Segment(_box.foot_right, border_style), - Segment(_box.foot_vertical, border_style), - ), - ( - Segment(_box.mid_left, border_style), - Segment(_box.mid_right, border_style), - Segment(_box.mid_vertical, border_style), - ), - ] - else: - box_segments = [] - - get_style = console.get_style - set_shape = self.set_shape - - for (first, last, unit) in loop_first_last(units): - header_row = first and show_header - footer_row = last - - if show_edge: - if header_row: - yield Segment(_box.get_top(widths, merge=True), border_style) - else: - yield Segment( - _box.get_row(widths, "head", edge=show_edge, cross_level="up"), - style=border_style, - ) - yield new_line - - # yield one unit header - header_style = get_style(unit.header_style or "") - lines = console.render_lines( - unit.header, sum(widths) + 1, style=header_style - ) - max_height = len(lines) - cells = [set_shape(max_height, lines, sum(widths) + 1)] - yield from self._render_line( - cells, max_height, box_segments, show_edge, is_header=True - ) - yield Segment( - _box.get_row(widths, "head", edge=show_edge, cross_level="down"), - style=border_style, - ) - yield new_line - - for row_cell in unit.kv.items(): - max_height = 1 - cells: List = [] - - for width, cell, style in zip(widths, row_cell, unit.kv_style): - lines = console.render_lines(cell, width, style=get_style(style)) - max_height = max(max_height, len(lines)) - cells.append(lines) - - cells[:] = [ - set_shape(max_height, cell, width) - for width, cell in zip(widths, cells) - ] - - yield from self._render_line(cells, max_height, box_segments, show_edge) - if _box and (show_lines): - yield Segment( - _box.get_row(widths, "row", edge=show_edge), - style=border_style, - ) - yield new_line - - if _box and show_edge: - yield Segment(_box.get_bottom(widths), style=border_style) - yield new_line - - def __render__(self, console: "Console") -> Generator: - if not self.units: - yield Segment.line() - return - - max_width = console.width - if self.width is not None: - max_width = self.width - - extra_width = self._extra_width - widths = self._calc_unit_widths(console, max_width - extra_width) - # print(widths) - table_width = sum(widths) + extra_width - - def render_annotation(text, style): - render_text = ( - console.render_str2(text, style=style) - if isinstance(text, str) - else text - ) - return render_text - - if self.title: - yield render_annotation( - (table_width - len(self.title)) // 2 * " " + self.title + "\n", - style=console.get_style(self.title_style or ""), - ) - yield from self._render(console, widths) - if self.caption: - yield render_annotation( - (table_width - len(self.caption)) // 2 * " " + self.caption + "\n", - style=console.get_style(self.caption_style or ""), - ) diff --git a/pigit/shellmode.py b/pigit/shellmode.py index 3199144..cc709f0 100644 --- a/pigit/shellmode.py +++ b/pigit/shellmode.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING, Callable, List, Optional, IO import os, cmd import functools -from pigit.render import get_console + +from plenty import get_console if TYPE_CHECKING: from pigit.gitlib.processor import ShortGitter diff --git a/pigit/tui/widgets.py b/pigit/tui/widgets.py index 18a37f6..2912270 100644 --- a/pigit/tui/widgets.py +++ b/pigit/tui/widgets.py @@ -4,10 +4,11 @@ import time from math import ceil +from plenty.str_utils import get_width +from plenty.style import Style + from .console import Term from ..common.utils import exec_cmd, confirm -from ..render.str_utils import get_width -from ..render.style import Fx class Widget(ABC): @@ -135,7 +136,7 @@ def generate_show_data( new_list = [] for line in raw_data: - text = Fx.uncolor(line) + text = Style.plain(line) count = sum(get_width(ord(ch)) for ch in text) # [float] is to solve the division of python2 without # retaining decimal places. diff --git a/requirements.txt b/requirements.txt index 6004032..2a40ac9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pylint pytest pytest-cov pyinstrument +plenty==1.0.2 diff --git a/setup.py b/setup.py index ae361db..a1ed30f 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", ], - install_requires=[], + install_requires=['plenty==1.0.2'], entry_points=""" [console_scripts] pigit=pigit.entry:main diff --git a/tests/test_counter.py b/tests/test_counter.py index 773fa9a..48db23e 100644 --- a/tests/test_counter.py +++ b/tests/test_counter.py @@ -4,7 +4,6 @@ import pytest from pigit.codecounter import CodeCounter -from pigit.render import get_console @pytest.mark.parametrize( diff --git a/tests/test_gitlib.py b/tests/test_gitlib.py index 8603538..b9df7bb 100644 --- a/tests/test_gitlib.py +++ b/tests/test_gitlib.py @@ -99,7 +99,7 @@ def setup(self): ], ) def test_color_command(self, setup, command: str): - from pigit.render import get_console + from plenty import get_console console = get_console() handle = setup diff --git a/tests/test_info.py b/tests/test_info.py index 029613f..5a5860f 100644 --- a/tests/test_info.py +++ b/tests/test_info.py @@ -3,7 +3,7 @@ from .utils import analyze_it from pigit.info import GitConfig, introduce -from pigit.render import get_console +from plenty import get_console console = get_console() diff --git a/tests/test_render.py b/tests/test_render.py deleted file mode 100644 index 767c0b6..0000000 --- a/tests/test_render.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- coding:utf-8 -*- -from typing import Optional -import pytest -import doctest - -from .utils import analyze_it - -from pigit.render.console import Console -from pigit.render.style import Color, Style -from pigit.render.markup import render_markup -from pigit.render.table import Table, UintTable -from pigit.render import box -import pigit.render.str_utils -from pigit.render.str_utils import byte_str2str - - -class TestColor: - @pytest.mark.parametrize( - ["input_", "wanted"], - [ - ["#FF0000", True], - ["#FF0", False], - ["#F0", True], - [[255, 0, 0], True], - [(255, 0, 0), True], - [[-1, 0, 0], False], - ["red", True], - [None, False], - [123456, False], - ], - ) - def test_is_color(self, input_, wanted): - assert Color.is_color(input_) == wanted - - @pytest.mark.parametrize( - "color", - [ - None, - "#FF0000", - "#f0", - ], - ) - def test_instance(self, color): - co = Color(color) - print(co.hexa, co.rgb, repr(co.escape)) - print(co) - - -class TestStyle: - @pytest.mark.parametrize( - "text", - [ - "Today is a b`nice` `day`.", - "Today is a b`nice`<#FF0000> day.", - "Today is a `nice` day.", - "Today is a `nice`<,sky_blue> day.", - "Today is a `nice`<> day.", - "Today is a b```nice``` day.", - "Today is a `nice`xxxxxxx day.", - "Today is a `nice` day.", - "Today is a (bold,underline)`nice` day.", - "Today is a (bold , underline)`nice` day.", - "Today is a (bold,underline`nice` day.", - "Today is a bold,underline)`nice` day.", - "i`Don't found Git, maybe need install.`tomato", - ], - ) - def test_style_render_style(self, text: str): - print("\n", Style.render_style(text)) - - @pytest.mark.parametrize( - "text", - [ - "Today is a b`nice` `day`.", - "Today is a b`nice`<#FF0000> day.", - "Today is a `nice` day.", - "Today is a `nice`<,sky_blue> day.", - "Today is a `nice`<> day.", - "Today is a b```nice``` day.", - "Today is a `nice`xxxxxxx day.", - "Today is a `nice` day.", - "i`Don't found Git, maybe need install.`tomato", - ], - ) - def test_style_remove_style(self, text: str): - print("\n", Style.remove_style(text)) - - @pytest.mark.parametrize( - ["color", "bg_color", "bold", "dark", "italic", "underline", "blink", "strick"], - [ - ["green", "", None, None, None, None, None, None], - ["#ff0000", "green", None, None, None, None, None, None], - ["", "yellow", None, None, None, None, None, None], - ["sky_blue", "", True, False, None, None, None, None], - ["sky_blue", "", None, True, None, None, None, None], - ["sky_blue", "", None, None, True, None, None, None], - ["sky_blue", "", None, None, None, True, None, None], - ["sky_blue", "", None, None, None, None, True, None], - ["sky_blue", "", None, None, None, None, None, True], - ["sky_blue", "", True, None, True, True, True, True], - ], - ) - def test_style_render( - self, color, bg_color, bold, dark, italic, underline, blink, strick - ): - style = Style( - color=color, - bg_color=bg_color, - bold=bold, - dark=dark, - italic=italic, - underline=underline, - blink=blink, - strick=strick, - ) - style.test() - - def test_style_add(self): - style1 = Style(color="green", bold=True) - style2 = Style(bg_color="red", bold=False, dark=True) - - print("\n", style1 + style2) - - @pytest.mark.parametrize( - "text", - [ - "Today is a b`nice` `day`. bye.", - ], - ) - def test_style_render_markup(self, text: str): - print("\n", text) - render_markup(text) - - -class TestTableModule: - def test_table(self): - console = Console() - # print(Text("`1234`")) - res_t = Table( - title="Search Result", - title_style="red", - # box=box.SIMPLE_HEAD, - caption="good table", - caption_style="purple dark", - border_style="red", - show_edge=False, - # show_lines=True - # show_header=False - ) - res_t.add_column("Idx", style="green") - res_t.add_column("Fiction Name", style="yellow") - res_t.add_column("Last Update", style="cyan") - res_t.add_column("Other Info") - - res_t.add_row("12", "34", "56", "1") - res_t.add_row("56", "`sun` is so big.", "10.dark`00`", "1") - res_t.add_row( - "我最棒", "9", "25", "100, this is a length test, get large length text." - ) - - console.echo(res_t, "`sun` is so big.") - - def test_unittable(self): - ut = UintTable( - title="unit table", - box=box.DOUBLE_EDGE, - border_style="sky_blue", - ) - - unit1 = ut.add_unit( - "Fruit true color", header_style="red bold", values_style="yellow" - ) - unit1.add_kv("apple", "red, this is a length test.\n second line.") - unit1.add_kv("grape", "purple") - - unit1 = ut.add_unit("Animal color") - unit1.add_kv("cattle", "yellow") - unit1.add_kv( - "sheep", "white, this is a length test, get large length text." * 10 - ) - - console = Console() - console.echo(ut) - - -class TestStrUtils: - def test_str_utils(self): - doctest.testmod(pigit.render.str_utils, verbose=True) - - def test_byte_str2str(self): - s = byte_str2str( - "test/\\346\\265\\213\\350\\257\\225\\344\\270\\255\\346\\226\\207.py" - ) - print(s) - assert s == "test/测试中文.py" - - -def test_console(): - console = Console() - console.echo([1, 2, 3, 4, 5])