556 lines
15 KiB
Python
556 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import enum
|
|
import typing
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from typing_extensions import Literal
|
|
|
|
|
|
# define some names for these constants to avoid misspellings in the source
|
|
# and to document the constant strings we are using
|
|
|
|
|
|
class Sizing(str, enum.Enum):
|
|
"""Widget sizing methods."""
|
|
|
|
FLOW = "flow"
|
|
BOX = "box"
|
|
FIXED = "fixed"
|
|
|
|
|
|
class Align(str, enum.Enum):
|
|
"""Text alignment modes"""
|
|
|
|
LEFT = "left"
|
|
RIGHT = "right"
|
|
CENTER = "center"
|
|
|
|
|
|
class VAlign(str, enum.Enum):
|
|
"""Filler alignment"""
|
|
|
|
TOP = "top"
|
|
MIDDLE = "middle"
|
|
BOTTOM = "bottom"
|
|
|
|
|
|
class WrapMode(str, enum.Enum):
|
|
"""Text wrapping modes"""
|
|
|
|
SPACE = "space"
|
|
ANY = "any"
|
|
CLIP = "clip"
|
|
ELLIPSIS = "ellipsis"
|
|
|
|
|
|
class WHSettings(str, enum.Enum):
|
|
"""Width and Height settings"""
|
|
|
|
PACK = "pack"
|
|
GIVEN = "given"
|
|
RELATIVE = "relative"
|
|
WEIGHT = "weight"
|
|
CLIP = "clip" # Used as "given" for widgets with fixed width (with clipping part of it)
|
|
FLOW = "flow" # Used as pack for flow widgets
|
|
|
|
|
|
RELATIVE_100 = (WHSettings.RELATIVE, 100)
|
|
|
|
|
|
@typing.overload
|
|
def normalize_align(
|
|
align: Literal["left", "center", "right"] | Align,
|
|
err: type[BaseException],
|
|
) -> tuple[Align, None]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_align(
|
|
align: tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
def normalize_align(
|
|
align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[Align, None] | tuple[Literal[WHSettings.RELATIVE], int]:
|
|
"""
|
|
Split align into (align_type, align_amount). Raise exception err
|
|
if align doesn't match a valid alignment.
|
|
"""
|
|
if align in {Align.LEFT, Align.CENTER, Align.RIGHT}:
|
|
return (Align(align), None)
|
|
|
|
if isinstance(align, tuple) and len(align) == 2 and align[0] == WHSettings.RELATIVE:
|
|
_align_type, align_amount = align
|
|
return (WHSettings.RELATIVE, align_amount)
|
|
|
|
raise err(
|
|
f"align value {align!r} is not one of 'left', 'center', 'right', ('relative', percentage 0=left 100=right)"
|
|
)
|
|
|
|
|
|
@typing.overload
|
|
def simplify_align(
|
|
align_type: Literal["relative", WHSettings.RELATIVE],
|
|
align_amount: int,
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_align(
|
|
align_type: Literal["relative", WHSettings.RELATIVE],
|
|
align_amount: None,
|
|
) -> typing.NoReturn: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_align(
|
|
align_type: Literal["left", "center", "right"] | Align,
|
|
align_amount: int | None,
|
|
) -> Align: ...
|
|
|
|
|
|
def simplify_align(
|
|
align_type: Literal["left", "center", "right", "relative", WHSettings.RELATIVE] | Align,
|
|
align_amount: int | None,
|
|
) -> Align | tuple[Literal[WHSettings.RELATIVE], int]:
|
|
"""
|
|
Recombine (align_type, align_amount) into an align value.
|
|
Inverse of normalize_align.
|
|
"""
|
|
if align_type == WHSettings.RELATIVE:
|
|
if not isinstance(align_amount, int):
|
|
raise TypeError(align_amount)
|
|
|
|
return (WHSettings.RELATIVE, align_amount)
|
|
return Align(align_type)
|
|
|
|
|
|
@typing.overload
|
|
def normalize_valign(
|
|
valign: Literal["top", "middle", "bottom"] | VAlign,
|
|
err: type[BaseException],
|
|
) -> tuple[VAlign, None]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_valign(
|
|
valign: tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
def normalize_valign(
|
|
valign: Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[VAlign, None] | tuple[Literal[WHSettings.RELATIVE], int]:
|
|
"""
|
|
Split align into (valign_type, valign_amount). Raise exception err
|
|
if align doesn't match a valid alignment.
|
|
"""
|
|
if valign in {VAlign.TOP, VAlign.MIDDLE, VAlign.BOTTOM}:
|
|
return (VAlign(valign), None)
|
|
|
|
if isinstance(valign, tuple) and len(valign) == 2 and valign[0] == WHSettings.RELATIVE:
|
|
_valign_type, valign_amount = valign
|
|
return (WHSettings.RELATIVE, valign_amount)
|
|
|
|
raise err(
|
|
f"valign value {valign!r} is not one of 'top', 'middle', 'bottom', ('relative', percentage 0=left 100=right)"
|
|
)
|
|
|
|
|
|
@typing.overload
|
|
def simplify_valign(
|
|
valign_type: Literal["top", "middle", "bottom"] | VAlign,
|
|
valign_amount: int | None,
|
|
) -> VAlign: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_valign(
|
|
valign_type: Literal["relative", WHSettings.RELATIVE],
|
|
valign_amount: int,
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_valign(
|
|
valign_type: Literal["relative", WHSettings.RELATIVE],
|
|
valign_amount: None,
|
|
) -> typing.NoReturn: ...
|
|
|
|
|
|
def simplify_valign(
|
|
valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
|
|
valign_amount: int | None,
|
|
) -> VAlign | tuple[Literal[WHSettings.RELATIVE], int]:
|
|
"""
|
|
Recombine (valign_type, valign_amount) into an valign value.
|
|
Inverse of normalize_valign.
|
|
"""
|
|
if valign_type == WHSettings.RELATIVE:
|
|
if not isinstance(valign_amount, int):
|
|
raise TypeError(valign_amount)
|
|
return (WHSettings.RELATIVE, valign_amount)
|
|
return VAlign(valign_type)
|
|
|
|
|
|
@typing.overload
|
|
def normalize_width(
|
|
width: Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.CLIP, WHSettings.PACK], None]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_width(
|
|
width: int,
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.GIVEN], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_width(
|
|
width: tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_width(
|
|
width: tuple[Literal["weight", WHSettings.WEIGHT], int | float],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...
|
|
|
|
|
|
def normalize_width(
|
|
width: (
|
|
Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK]
|
|
| int
|
|
| tuple[Literal["relative", WHSettings.RELATIVE], int]
|
|
| tuple[Literal["weight", WHSettings.WEIGHT], int | float]
|
|
),
|
|
err: type[BaseException],
|
|
) -> (
|
|
tuple[Literal[WHSettings.CLIP, WHSettings.PACK], None]
|
|
| tuple[Literal[WHSettings.GIVEN, WHSettings.RELATIVE], int]
|
|
| tuple[Literal[WHSettings.WEIGHT], int | float]
|
|
):
|
|
"""
|
|
Split width into (width_type, width_amount). Raise exception err
|
|
if width doesn't match a valid alignment.
|
|
"""
|
|
if width in {WHSettings.CLIP, WHSettings.PACK}:
|
|
return (WHSettings(width), None)
|
|
|
|
if isinstance(width, int):
|
|
return (WHSettings.GIVEN, width)
|
|
|
|
if isinstance(width, tuple) and len(width) == 2 and width[0] in {WHSettings.RELATIVE, WHSettings.WEIGHT}:
|
|
width_type, width_amount = width
|
|
return (WHSettings(width_type), width_amount)
|
|
|
|
raise err(
|
|
f"width value {width!r} is not one of"
|
|
f"fixed number of columns, 'pack', ('relative', percentage of total width), 'clip'"
|
|
)
|
|
|
|
|
|
@typing.overload
|
|
def simplify_width(
|
|
width_type: Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK],
|
|
width_amount: int | None,
|
|
) -> Literal[WHSettings.CLIP, WHSettings.PACK]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_width(
|
|
width_type: Literal["given", WHSettings.GIVEN],
|
|
width_amount: int,
|
|
) -> int: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_width(
|
|
width_type: Literal["relative", WHSettings.RELATIVE],
|
|
width_amount: int,
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_width(
|
|
width_type: Literal["weight", WHSettings.WEIGHT],
|
|
width_amount: int | float, # noqa: PYI041 # provide explicit for IDEs
|
|
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_width(
|
|
width_type: Literal["given", "relative", "weight", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.WEIGHT],
|
|
width_amount: None,
|
|
) -> typing.NoReturn: ...
|
|
|
|
|
|
def simplify_width(
|
|
width_type: Literal["clip", "pack", "given", "relative", "weight"] | WHSettings,
|
|
width_amount: int | float | None, # noqa: PYI041 # provide explicit for IDEs
|
|
) -> (
|
|
Literal[WHSettings.CLIP, WHSettings.PACK]
|
|
| int
|
|
| tuple[Literal[WHSettings.RELATIVE], int]
|
|
| tuple[Literal[WHSettings.WEIGHT], int | float]
|
|
):
|
|
"""
|
|
Recombine (width_type, width_amount) into an width value.
|
|
Inverse of normalize_width.
|
|
"""
|
|
if width_type in {WHSettings.CLIP, WHSettings.PACK}:
|
|
return WHSettings(width_type)
|
|
|
|
if not isinstance(width_amount, int):
|
|
raise TypeError(width_amount)
|
|
|
|
if width_type == WHSettings.GIVEN:
|
|
return width_amount
|
|
|
|
return (WHSettings(width_type), width_amount)
|
|
|
|
|
|
@typing.overload
|
|
def normalize_height(
|
|
height: int,
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.GIVEN], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_height(
|
|
height: Literal["flow", "pack", Sizing.FLOW, WHSettings.PACK],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[Sizing.FLOW, WHSettings.PACK], None]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_height(
|
|
height: tuple[Literal["relative", WHSettings.RELATIVE], int],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def normalize_height(
|
|
height: tuple[Literal["weight", WHSettings.WEIGHT], int | float],
|
|
err: type[BaseException],
|
|
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...
|
|
|
|
|
|
def normalize_height(
|
|
height: (
|
|
int
|
|
| Literal["flow", "pack", Sizing.FLOW, WHSettings.PACK]
|
|
| tuple[Literal["relative", WHSettings.RELATIVE], int]
|
|
| tuple[Literal["weight", WHSettings.WEIGHT], int | float]
|
|
),
|
|
err: type[BaseException],
|
|
) -> (
|
|
tuple[Literal[Sizing.FLOW, WHSettings.PACK], None]
|
|
| tuple[Literal[WHSettings.RELATIVE, WHSettings.GIVEN], int]
|
|
| tuple[Literal[WHSettings.WEIGHT], int | float]
|
|
):
|
|
"""
|
|
Split height into (height_type, height_amount). Raise exception err
|
|
if height isn't valid.
|
|
"""
|
|
if height == Sizing.FLOW:
|
|
return (Sizing.FLOW, None)
|
|
|
|
if height == WHSettings.PACK:
|
|
return (WHSettings.PACK, None)
|
|
|
|
if isinstance(height, tuple) and len(height) == 2 and height[0] in {WHSettings.RELATIVE, WHSettings.WEIGHT}:
|
|
return (WHSettings(height[0]), height[1])
|
|
|
|
if isinstance(height, int):
|
|
return (WHSettings.GIVEN, height)
|
|
|
|
raise err(
|
|
f"height value {height!r} is not one of "
|
|
f"fixed number of columns, 'pack', ('relative', percentage of total height)"
|
|
)
|
|
|
|
|
|
@typing.overload
|
|
def simplify_height(
|
|
height_type: Literal["flow", "pack", WHSettings.FLOW, WHSettings.PACK],
|
|
height_amount: int | None,
|
|
) -> Literal[WHSettings.FLOW, WHSettings.PACK]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_height(
|
|
height_type: Literal["given", WHSettings.GIVEN],
|
|
height_amount: int,
|
|
) -> int: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_height(
|
|
height_type: Literal["relative", WHSettings.RELATIVE],
|
|
height_amount: int | None,
|
|
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_height(
|
|
height_type: Literal["weight", WHSettings.WEIGHT],
|
|
height_amount: int | float | None, # noqa: PYI041 # provide explicit for IDEs
|
|
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...
|
|
|
|
|
|
@typing.overload
|
|
def simplify_height(
|
|
height_type: Literal["relative", "given", "weight", WHSettings.RELATIVE, WHSettings.GIVEN, WHSettings.WEIGHT],
|
|
height_amount: None,
|
|
) -> typing.NoReturn: ...
|
|
|
|
|
|
def simplify_height(
|
|
height_type: Literal[
|
|
"flow",
|
|
"pack",
|
|
"relative",
|
|
"given",
|
|
"weight",
|
|
WHSettings.FLOW,
|
|
WHSettings.PACK,
|
|
WHSettings.RELATIVE,
|
|
WHSettings.GIVEN,
|
|
WHSettings.WEIGHT,
|
|
],
|
|
height_amount: int | float | None, # noqa: PYI041 # provide explicit for IDEs
|
|
) -> (
|
|
int
|
|
| Literal[WHSettings.FLOW, WHSettings.PACK]
|
|
| tuple[Literal[WHSettings.RELATIVE], int]
|
|
| tuple[Literal[WHSettings.WEIGHT], int | float]
|
|
):
|
|
"""
|
|
Recombine (height_type, height_amount) into a height value.
|
|
Inverse of normalize_height.
|
|
"""
|
|
if height_type in {WHSettings.FLOW, WHSettings.PACK}:
|
|
return WHSettings(height_type)
|
|
|
|
if not isinstance(height_amount, int):
|
|
raise TypeError(height_amount)
|
|
|
|
if height_type == WHSettings.GIVEN:
|
|
return height_amount
|
|
|
|
return (WHSettings(height_type), height_amount)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class _BoxSymbols:
|
|
"""Box symbols for drawing."""
|
|
|
|
HORIZONTAL: str
|
|
VERTICAL: str
|
|
TOP_LEFT: str
|
|
TOP_RIGHT: str
|
|
BOTTOM_LEFT: str
|
|
BOTTOM_RIGHT: str
|
|
# Joints for tables making
|
|
LEFT_T: str
|
|
RIGHT_T: str
|
|
TOP_T: str
|
|
BOTTOM_T: str
|
|
CROSS: str
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class _BoxSymbolsWithDashes(_BoxSymbols):
|
|
"""Box symbols for drawing.
|
|
|
|
Extra dashes symbols.
|
|
"""
|
|
|
|
HORIZONTAL_4_DASHES: str
|
|
HORIZONTAL_3_DASHES: str
|
|
HORIZONTAL_2_DASHES: str
|
|
VERTICAL_2_DASH: str
|
|
VERTICAL_3_DASH: str
|
|
VERTICAL_4_DASH: str
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class _LightBoxSymbols(_BoxSymbolsWithDashes):
|
|
"""Box symbols for drawing.
|
|
|
|
The Thin version includes extra symbols.
|
|
Symbols are ordered as in Unicode except dashes.
|
|
"""
|
|
|
|
TOP_LEFT_ROUNDED: str
|
|
TOP_RIGHT_ROUNDED: str
|
|
BOTTOM_LEFT_ROUNDED: str
|
|
BOTTOM_RIGHT_ROUNDED: str
|
|
|
|
|
|
class _BoxSymbolsCollection(typing.NamedTuple):
|
|
"""Standard Unicode box symbols for basic tables drawing.
|
|
|
|
.. note::
|
|
Transitions are not included: depends on line types, different kinds of transitions are available.
|
|
Please check Unicode table for transitions symbols if required.
|
|
"""
|
|
|
|
# fmt: off
|
|
|
|
LIGHT: _LightBoxSymbols = _LightBoxSymbols(
|
|
"─", "│", "┌", "┐", "└", "┘", "├", "┤", "┬", "┴", "┼", "┈", "┄", "╌", "╎", "┆", "┊", "╭", "╮", "╰", "╯"
|
|
)
|
|
HEAVY: _BoxSymbolsWithDashes = _BoxSymbolsWithDashes(
|
|
"━", "┃", "┏", "┓", "┗", "┛", "┣", "┫", "┳", "┻", "╋", "┉", "┅", "╍", "╏", "┇", "┋"
|
|
)
|
|
DOUBLE: _BoxSymbols = _BoxSymbols(
|
|
"═", "║", "╔", "╗", "╚", "╝", "╠", "╣", "╦", "╩", "╬"
|
|
)
|
|
|
|
|
|
BOX_SYMBOLS = _BoxSymbolsCollection()
|
|
|
|
|
|
class BAR_SYMBOLS(str, enum.Enum):
|
|
"""Standard Unicode bar symbols excluding empty space.
|
|
|
|
Start from space (0), then 1/8 till full block (1/1).
|
|
Typically used only 8 from this symbol collection depends on use-case:
|
|
* empty - 7/8 and styles for BG different on both sides (like standard `ProgressBar` and `BarGraph`)
|
|
* 1/8 - full block and single style for BG on the right side
|
|
"""
|
|
|
|
# fmt: off
|
|
|
|
HORISONTAL = " ▏▎▍▌▋▊▉█"
|
|
VERTICAL = " ▁▂▃▄▅▆▇█"
|
|
|
|
|
|
class _SHADE_SYMBOLS(typing.NamedTuple):
|
|
"""Standard shade symbols excluding empty space."""
|
|
|
|
# fmt: off
|
|
|
|
FULL_BLOCK: str = "█"
|
|
DARK_SHADE: str = "▓"
|
|
MEDIUM_SHADE: str = "▒"
|
|
LITE_SHADE: str = "░"
|
|
|
|
|
|
SHADE_SYMBOLS = _SHADE_SYMBOLS()
|