Aluode/PerceptionLabPortable
0
1from typing import TYPE_CHECKING, Iterable, List, Literal2 3 4from ._loop import loop_last5 6if TYPE_CHECKING:7 from pip._vendor.rich.console import ConsoleOptions8 9 10class Box:11 """Defines characters to render boxes.12 13 ┌─┬┐ top14 │ ││ head15 ├─┼┤ head_row16 │ ││ mid17 ├─┼┤ row18 ├─┼┤ foot_row19 │ ││ foot20 └─┴┘ bottom21 22 Args:23 box (str): Characters making up box.24 ascii (bool, optional): True if this box uses ascii characters only. Default is False.25 """26 27 def __init__(self, box: str, *, ascii: bool = False) -> None:28 self._box = box29 self.ascii = ascii30 line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()31 # top32 self.top_left, self.top, self.top_divider, self.top_right = iter(line1)33 # head34 self.head_left, _, self.head_vertical, self.head_right = iter(line2)35 # head_row36 (37 self.head_row_left,38 self.head_row_horizontal,39 self.head_row_cross,40 self.head_row_right,41 ) = iter(line3)42 43 # mid44 self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)45 # row46 self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)47 # foot_row48 (49 self.foot_row_left,50 self.foot_row_horizontal,51 self.foot_row_cross,52 self.foot_row_right,53 ) = iter(line6)54 # foot55 self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)56 # bottom57 self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(58 line859 )60 61 def __repr__(self) -> str:62 return "Box(...)"63 64 def __str__(self) -> str:65 return self._box66 67 def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":68 """Substitute this box for another if it won't render due to platform issues.69 70 Args:71 options (ConsoleOptions): Console options used in rendering.72 safe (bool, optional): Substitute this for another Box if there are known problems73 displaying on the platform (currently only relevant on Windows). Default is True.74 75 Returns:76 Box: A different Box or the same Box.77 """78 box = self79 if options.legacy_windows and safe:80 box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)81 if options.ascii_only and not box.ascii:82 box = ASCII83 return box84 85 def get_plain_headed_box(self) -> "Box":86 """If this box uses special characters for the borders of the header, then87 return the equivalent box that does not.88 89 Returns:90 Box: The most similar Box that doesn't use header-specific box characters.91 If the current Box already satisfies this criterion, then it's returned.92 """93 return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)94 95 def get_top(self, widths: Iterable[int]) -> str:96 """Get the top of a simple box.97 98 Args:99 widths (List[int]): Widths of columns.100 101 Returns:102 str: A string of box characters.103 """104 105 parts: List[str] = []106 append = parts.append107 append(self.top_left)108 for last, width in loop_last(widths):109 append(self.top * width)110 if not last:111 append(self.top_divider)112 append(self.top_right)113 return "".join(parts)114 115 def get_row(116 self,117 widths: Iterable[int],118 level: Literal["head", "row", "foot", "mid"] = "row",119 edge: bool = True,120 ) -> str:121 """Get the top of a simple box.122 123 Args:124 width (List[int]): Widths of columns.125 126 Returns:127 str: A string of box characters.128 """129 if level == "head":130 left = self.head_row_left131 horizontal = self.head_row_horizontal132 cross = self.head_row_cross133 right = self.head_row_right134 elif level == "row":135 left = self.row_left136 horizontal = self.row_horizontal137 cross = self.row_cross138 right = self.row_right139 elif level == "mid":140 left = self.mid_left141 horizontal = " "142 cross = self.mid_vertical143 right = self.mid_right144 elif level == "foot":145 left = self.foot_row_left146 horizontal = self.foot_row_horizontal147 cross = self.foot_row_cross148 right = self.foot_row_right149 else:150 raise ValueError("level must be 'head', 'row' or 'foot'")151 152 parts: List[str] = []153 append = parts.append154 if edge:155 append(left)156 for last, width in loop_last(widths):157 append(horizontal * width)158 if not last:159 append(cross)160 if edge:161 append(right)162 return "".join(parts)163 164 def get_bottom(self, widths: Iterable[int]) -> str:165 """Get the bottom of a simple box.166 167 Args:168 widths (List[int]): Widths of columns.169 170 Returns:171 str: A string of box characters.172 """173 174 parts: List[str] = []175 append = parts.append176 append(self.bottom_left)177 for last, width in loop_last(widths):178 append(self.bottom * width)179 if not last:180 append(self.bottom_divider)181 append(self.bottom_right)182 return "".join(parts)183 184 185# fmt: off186ASCII: Box = Box(187 "+--+\n"188 "| ||\n"189 "|-+|\n"190 "| ||\n"191 "|-+|\n"192 "|-+|\n"193 "| ||\n"194 "+--+\n",195 ascii=True,196)197 198ASCII2: Box = Box(199 "+-++\n"200 "| ||\n"201 "+-++\n"202 "| ||\n"203 "+-++\n"204 "+-++\n"205 "| ||\n"206 "+-++\n",207 ascii=True,208)209 210ASCII_DOUBLE_HEAD: Box = Box(211 "+-++\n"212 "| ||\n"213 "+=++\n"214 "| ||\n"215 "+-++\n"216 "+-++\n"217 "| ||\n"218 "+-++\n",219 ascii=True,220)221 222SQUARE: Box = Box(223 "┌─┬┐\n"224 "│ ││\n"225 "├─┼┤\n"226 "│ ││\n"227 "├─┼┤\n"228 "├─┼┤\n"229 "│ ││\n"230 "└─┴┘\n"231)232 233SQUARE_DOUBLE_HEAD: Box = Box(234 "┌─┬┐\n"235 "│ ││\n"236 "╞═╪╡\n"237 "│ ││\n"238 "├─┼┤\n"239 "├─┼┤\n"240 "│ ││\n"241 "└─┴┘\n"242)243 244MINIMAL: Box = Box(245 " ╷ \n"246 " │ \n"247 "╶─┼╴\n"248 " │ \n"249 "╶─┼╴\n"250 "╶─┼╴\n"251 " │ \n"252 " ╵ \n"253)254 255 256MINIMAL_HEAVY_HEAD: Box = Box(257 " ╷ \n"258 " │ \n"259 "╺━┿╸\n"260 " │ \n"261 "╶─┼╴\n"262 "╶─┼╴\n"263 " │ \n"264 " ╵ \n"265)266 267MINIMAL_DOUBLE_HEAD: Box = Box(268 " ╷ \n"269 " │ \n"270 " ═╪ \n"271 " │ \n"272 " ─┼ \n"273 " ─┼ \n"274 " │ \n"275 " ╵ \n"276)277 278 279SIMPLE: Box = Box(280 " \n"281 " \n"282 " ── \n"283 " \n"284 " \n"285 " ── \n"286 " \n"287 " \n"288)289 290SIMPLE_HEAD: Box = Box(291 " \n"292 " \n"293 " ── \n"294 " \n"295 " \n"296 " \n"297 " \n"298 " \n"299)300 301 302SIMPLE_HEAVY: Box = Box(303 " \n"304 " \n"305 " ━━ \n"306 " \n"307 " \n"308 " ━━ \n"309 " \n"310 " \n"311)312 313 314HORIZONTALS: Box = Box(315 " ── \n"316 " \n"317 " ── \n"318 " \n"319 " ── \n"320 " ── \n"321 " \n"322 " ── \n"323)324 325ROUNDED: Box = Box(326 "╭─┬╮\n"327 "│ ││\n"328 "├─┼┤\n"329 "│ ││\n"330 "├─┼┤\n"331 "├─┼┤\n"332 "│ ││\n"333 "╰─┴╯\n"334)335 336HEAVY: Box = Box(337 "┏━┳┓\n"338 "┃ ┃┃\n"339 "┣━╋┫\n"340 "┃ ┃┃\n"341 "┣━╋┫\n"342 "┣━╋┫\n"343 "┃ ┃┃\n"344 "┗━┻┛\n"345)346 347HEAVY_EDGE: Box = Box(348 "┏━┯┓\n"349 "┃ │┃\n"350 "┠─┼┨\n"351 "┃ │┃\n"352 "┠─┼┨\n"353 "┠─┼┨\n"354 "┃ │┃\n"355 "┗━┷┛\n"356)357 358HEAVY_HEAD: Box = Box(359 "┏━┳┓\n"360 "┃ ┃┃\n"361 "┡━╇┩\n"362 "│ ││\n"363 "├─┼┤\n"364 "├─┼┤\n"365 "│ ││\n"366 "└─┴┘\n"367)368 369DOUBLE: Box = Box(370 "╔═╦╗\n"371 "║ ║║\n"372 "╠═╬╣\n"373 "║ ║║\n"374 "╠═╬╣\n"375 "╠═╬╣\n"376 "║ ║║\n"377 "╚═╩╝\n"378)379 380DOUBLE_EDGE: Box = Box(381 "╔═╤╗\n"382 "║ │║\n"383 "╟─┼╢\n"384 "║ │║\n"385 "╟─┼╢\n"386 "╟─┼╢\n"387 "║ │║\n"388 "╚═╧╝\n"389)390 391MARKDOWN: Box = Box(392 " \n"393 "| ||\n"394 "|-||\n"395 "| ||\n"396 "|-||\n"397 "|-||\n"398 "| ||\n"399 " \n",400 ascii=True,401)402# fmt: on403 404# Map Boxes that don't render with raster fonts on to equivalent that do405LEGACY_WINDOWS_SUBSTITUTIONS = {406 ROUNDED: SQUARE,407 MINIMAL_HEAVY_HEAD: MINIMAL,408 SIMPLE_HEAVY: SIMPLE,409 HEAVY: SQUARE,410 HEAVY_EDGE: SQUARE,411 HEAVY_HEAD: SQUARE,412}413 414# Map headed boxes to their headerless equivalents415PLAIN_HEADED_SUBSTITUTIONS = {416 HEAVY_HEAD: SQUARE,417 SQUARE_DOUBLE_HEAD: SQUARE,418 MINIMAL_DOUBLE_HEAD: MINIMAL,419 MINIMAL_HEAVY_HEAD: MINIMAL,420 ASCII_DOUBLE_HEAD: ASCII2,421}422 423 424if __name__ == "__main__": # pragma: no cover425 from pip._vendor.rich.columns import Columns426 from pip._vendor.rich.panel import Panel427 428 from . import box as box429 from .console import Console430 from .table import Table431 from .text import Text432 433 console = Console(record=True)434 435 BOXES = [436 "ASCII",437 "ASCII2",438 "ASCII_DOUBLE_HEAD",439 "SQUARE",440 "SQUARE_DOUBLE_HEAD",441 "MINIMAL",442 "MINIMAL_HEAVY_HEAD",443 "MINIMAL_DOUBLE_HEAD",444 "SIMPLE",445 "SIMPLE_HEAD",446 "SIMPLE_HEAVY",447 "HORIZONTALS",448 "ROUNDED",449 "HEAVY",450 "HEAVY_EDGE",451 "HEAVY_HEAD",452 "DOUBLE",453 "DOUBLE_EDGE",454 "MARKDOWN",455 ]456 457 console.print(Panel("[bold green]Box Constants", style="green"), justify="center")458 console.print()459 460 columns = Columns(expand=True, padding=2)461 for box_name in sorted(BOXES):462 table = Table(463 show_footer=True, style="dim", border_style="not dim", expand=True464 )465 table.add_column("Header 1", "Footer 1")466 table.add_column("Header 2", "Footer 2")467 table.add_row("Cell", "Cell")468 table.add_row("Cell", "Cell")469 table.box = getattr(box, box_name)470 table.title = Text(f"box.{box_name}", style="magenta")471 columns.add_renderable(table)472 console.print(columns)473 474 # console.save_svg("box.svg")475 