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()