codekingpro/portable-devtools
114k
1import sys2from typing import TYPE_CHECKING, Iterable, List3 4if sys.version_info >= (3, 8):5 from typing import Literal6else:7 from typing_extensions import Literal # pragma: no cover8 9 10from ._loop import loop_last11 12if TYPE_CHECKING:13 from rich.console import ConsoleOptions14 15 16class Box:17 """Defines characters to render boxes.18 19 ┌─┬┐ top20 │ ││ head21 ├─┼┤ head_row22 │ ││ mid23 ├─┼┤ row24 ├─┼┤ foot_row25 │ ││ foot26 └─┴┘ bottom27 28 Args:29 box (str): Characters making up box.30 ascii (bool, optional): True if this box uses ascii characters only. Default is False.31 """32 33 def __init__(self, box: str, *, ascii: bool = False) -> None:34 self._box = box35 self.ascii = ascii36 line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()37 # top38 self.top_left, self.top, self.top_divider, self.top_right = iter(line1)39 # head40 self.head_left, _, self.head_vertical, self.head_right = iter(line2)41 # head_row42 (43 self.head_row_left,44 self.head_row_horizontal,45 self.head_row_cross,46 self.head_row_right,47 ) = iter(line3)48 49 # mid50 self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)51 # row52 self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)53 # foot_row54 (55 self.foot_row_left,56 self.foot_row_horizontal,57 self.foot_row_cross,58 self.foot_row_right,59 ) = iter(line6)60 # foot61 self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)62 # bottom63 self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(64 line865 )66 67 def __repr__(self) -> str:68 return "Box(...)"69 70 def __str__(self) -> str:71 return self._box72 73 def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":74 """Substitute this box for another if it won't render due to platform issues.75 76 Args:77 options (ConsoleOptions): Console options used in rendering.78 safe (bool, optional): Substitute this for another Box if there are known problems79 displaying on the platform (currently only relevant on Windows). Default is True.80 81 Returns:82 Box: A different Box or the same Box.83 """84 box = self85 if options.legacy_windows and safe:86 box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)87 if options.ascii_only and not box.ascii:88 box = ASCII89 return box90 91 def get_plain_headed_box(self) -> "Box":92 """If this box uses special characters for the borders of the header, then93 return the equivalent box that does not.94 95 Returns:96 Box: The most similar Box that doesn't use header-specific box characters.97 If the current Box already satisfies this criterion, then it's returned.98 """99 return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)100 101 def get_top(self, widths: Iterable[int]) -> str:102 """Get the top of a simple box.103 104 Args:105 widths (List[int]): Widths of columns.106 107 Returns:108 str: A string of box characters.109 """110 111 parts: List[str] = []112 append = parts.append113 append(self.top_left)114 for last, width in loop_last(widths):115 append(self.top * width)116 if not last:117 append(self.top_divider)118 append(self.top_right)119 return "".join(parts)120 121 def get_row(122 self,123 widths: Iterable[int],124 level: Literal["head", "row", "foot", "mid"] = "row",125 edge: bool = True,126 ) -> str:127 """Get the top of a simple box.128 129 Args:130 width (List[int]): Widths of columns.131 132 Returns:133 str: A string of box characters.134 """135 if level == "head":136 left = self.head_row_left137 horizontal = self.head_row_horizontal138 cross = self.head_row_cross139 right = self.head_row_right140 elif level == "row":141 left = self.row_left142 horizontal = self.row_horizontal143 cross = self.row_cross144 right = self.row_right145 elif level == "mid":146 left = self.mid_left147 horizontal = " "148 cross = self.mid_vertical149 right = self.mid_right150 elif level == "foot":151 left = self.foot_row_left152 horizontal = self.foot_row_horizontal153 cross = self.foot_row_cross154 right = self.foot_row_right155 else:156 raise ValueError("level must be 'head', 'row' or 'foot'")157 158 parts: List[str] = []159 append = parts.append160 if edge:161 append(left)162 for last, width in loop_last(widths):163 append(horizontal * width)164 if not last:165 append(cross)166 if edge:167 append(right)168 return "".join(parts)169 170 def get_bottom(self, widths: Iterable[int]) -> str:171 """Get the bottom of a simple box.172 173 Args:174 widths (List[int]): Widths of columns.175 176 Returns:177 str: A string of box characters.178 """179 180 parts: List[str] = []181 append = parts.append182 append(self.bottom_left)183 for last, width in loop_last(widths):184 append(self.bottom * width)185 if not last:186 append(self.bottom_divider)187 append(self.bottom_right)188 return "".join(parts)189 190 191# fmt: off192ASCII: Box = Box(193 "+--+\n"194 "| ||\n"195 "|-+|\n"196 "| ||\n"197 "|-+|\n"198 "|-+|\n"199 "| ||\n"200 "+--+\n",201 ascii=True,202)203 204ASCII2: Box = Box(205 "+-++\n"206 "| ||\n"207 "+-++\n"208 "| ||\n"209 "+-++\n"210 "+-++\n"211 "| ||\n"212 "+-++\n",213 ascii=True,214)215 216ASCII_DOUBLE_HEAD: Box = Box(217 "+-++\n"218 "| ||\n"219 "+=++\n"220 "| ||\n"221 "+-++\n"222 "+-++\n"223 "| ||\n"224 "+-++\n",225 ascii=True,226)227 228SQUARE: Box = Box(229 "┌─┬┐\n"230 "│ ││\n"231 "├─┼┤\n"232 "│ ││\n"233 "├─┼┤\n"234 "├─┼┤\n"235 "│ ││\n"236 "└─┴┘\n"237)238 239SQUARE_DOUBLE_HEAD: Box = Box(240 "┌─┬┐\n"241 "│ ││\n"242 "╞═╪╡\n"243 "│ ││\n"244 "├─┼┤\n"245 "├─┼┤\n"246 "│ ││\n"247 "└─┴┘\n"248)249 250MINIMAL: Box = Box(251 " ╷ \n"252 " │ \n"253 "╶─┼╴\n"254 " │ \n"255 "╶─┼╴\n"256 "╶─┼╴\n"257 " │ \n"258 " ╵ \n"259)260 261 262MINIMAL_HEAVY_HEAD: Box = Box(263 " ╷ \n"264 " │ \n"265 "╺━┿╸\n"266 " │ \n"267 "╶─┼╴\n"268 "╶─┼╴\n"269 " │ \n"270 " ╵ \n"271)272 273MINIMAL_DOUBLE_HEAD: Box = Box(274 " ╷ \n"275 " │ \n"276 " ═╪ \n"277 " │ \n"278 " ─┼ \n"279 " ─┼ \n"280 " │ \n"281 " ╵ \n"282)283 284 285SIMPLE: Box = Box(286 " \n"287 " \n"288 " ── \n"289 " \n"290 " \n"291 " ── \n"292 " \n"293 " \n"294)295 296SIMPLE_HEAD: Box = Box(297 " \n"298 " \n"299 " ── \n"300 " \n"301 " \n"302 " \n"303 " \n"304 " \n"305)306 307 308SIMPLE_HEAVY: Box = Box(309 " \n"310 " \n"311 " ━━ \n"312 " \n"313 " \n"314 " ━━ \n"315 " \n"316 " \n"317)318 319 320HORIZONTALS: Box = Box(321 " ── \n"322 " \n"323 " ── \n"324 " \n"325 " ── \n"326 " ── \n"327 " \n"328 " ── \n"329)330 331ROUNDED: Box = Box(332 "╭─┬╮\n"333 "│ ││\n"334 "├─┼┤\n"335 "│ ││\n"336 "├─┼┤\n"337 "├─┼┤\n"338 "│ ││\n"339 "╰─┴╯\n"340)341 342HEAVY: Box = Box(343 "┏━┳┓\n"344 "┃ ┃┃\n"345 "┣━╋┫\n"346 "┃ ┃┃\n"347 "┣━╋┫\n"348 "┣━╋┫\n"349 "┃ ┃┃\n"350 "┗━┻┛\n"351)352 353HEAVY_EDGE: Box = Box(354 "┏━┯┓\n"355 "┃ │┃\n"356 "┠─┼┨\n"357 "┃ │┃\n"358 "┠─┼┨\n"359 "┠─┼┨\n"360 "┃ │┃\n"361 "┗━┷┛\n"362)363 364HEAVY_HEAD: Box = Box(365 "┏━┳┓\n"366 "┃ ┃┃\n"367 "┡━╇┩\n"368 "│ ││\n"369 "├─┼┤\n"370 "├─┼┤\n"371 "│ ││\n"372 "└─┴┘\n"373)374 375DOUBLE: Box = Box(376 "╔═╦╗\n"377 "║ ║║\n"378 "╠═╬╣\n"379 "║ ║║\n"380 "╠═╬╣\n"381 "╠═╬╣\n"382 "║ ║║\n"383 "╚═╩╝\n"384)385 386DOUBLE_EDGE: Box = Box(387 "╔═╤╗\n"388 "║ │║\n"389 "╟─┼╢\n"390 "║ │║\n"391 "╟─┼╢\n"392 "╟─┼╢\n"393 "║ │║\n"394 "╚═╧╝\n"395)396 397MARKDOWN: Box = Box(398 " \n"399 "| ||\n"400 "|-||\n"401 "| ||\n"402 "|-||\n"403 "|-||\n"404 "| ||\n"405 " \n",406 ascii=True,407)408# fmt: on409 410# Map Boxes that don't render with raster fonts on to equivalent that do411LEGACY_WINDOWS_SUBSTITUTIONS = {412 ROUNDED: SQUARE,413 MINIMAL_HEAVY_HEAD: MINIMAL,414 SIMPLE_HEAVY: SIMPLE,415 HEAVY: SQUARE,416 HEAVY_EDGE: SQUARE,417 HEAVY_HEAD: SQUARE,418}419 420# Map headed boxes to their headerless equivalents421PLAIN_HEADED_SUBSTITUTIONS = {422 HEAVY_HEAD: SQUARE,423 SQUARE_DOUBLE_HEAD: SQUARE,424 MINIMAL_DOUBLE_HEAD: MINIMAL,425 MINIMAL_HEAVY_HEAD: MINIMAL,426 ASCII_DOUBLE_HEAD: ASCII2,427}428 429 430if __name__ == "__main__": # pragma: no cover431 from rich.columns import Columns432 from rich.panel import Panel433 434 from . import box as box435 from .console import Console436 from .table import Table437 from .text import Text438 439 console = Console(record=True)440 441 BOXES = [442 "ASCII",443 "ASCII2",444 "ASCII_DOUBLE_HEAD",445 "SQUARE",446 "SQUARE_DOUBLE_HEAD",447 "MINIMAL",448 "MINIMAL_HEAVY_HEAD",449 "MINIMAL_DOUBLE_HEAD",450 "SIMPLE",451 "SIMPLE_HEAD",452 "SIMPLE_HEAVY",453 "HORIZONTALS",454 "ROUNDED",455 "HEAVY",456 "HEAVY_EDGE",457 "HEAVY_HEAD",458 "DOUBLE",459 "DOUBLE_EDGE",460 "MARKDOWN",461 ]462 463 console.print(Panel("[bold green]Box Constants", style="green"), justify="center")464 console.print()465 466 columns = Columns(expand=True, padding=2)467 for box_name in sorted(BOXES):468 table = Table(469 show_footer=True, style="dim", border_style="not dim", expand=True470 )471 table.add_column("Header 1", "Footer 1")472 table.add_column("Header 2", "Footer 2")473 table.add_row("Cell", "Cell")474 table.add_row("Cell", "Cell")475 table.box = getattr(box, box_name)476 table.title = Text(f"box.{box_name}", style="magenta")477 columns.add_renderable(table)478 console.print(columns)479 480 # console.save_svg("box.svg")481 