您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

475 行
10 KiB

  1. from typing import TYPE_CHECKING, Iterable, List, Literal
  2. from ._loop import loop_last
  3. if TYPE_CHECKING:
  4. from rich.console import ConsoleOptions
  5. class Box:
  6. """Defines characters to render boxes.
  7. ┌─┬┐ top
  8. │ ││ head
  9. ├─┼┤ head_row
  10. │ ││ mid
  11. ├─┼┤ row
  12. ├─┼┤ foot_row
  13. │ ││ foot
  14. └─┴┘ bottom
  15. Args:
  16. box (str): Characters making up box.
  17. ascii (bool, optional): True if this box uses ascii characters only. Default is False.
  18. """
  19. def __init__(self, box: str, *, ascii: bool = False) -> None:
  20. self._box = box
  21. self.ascii = ascii
  22. line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
  23. # top
  24. self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
  25. # head
  26. self.head_left, _, self.head_vertical, self.head_right = iter(line2)
  27. # head_row
  28. (
  29. self.head_row_left,
  30. self.head_row_horizontal,
  31. self.head_row_cross,
  32. self.head_row_right,
  33. ) = iter(line3)
  34. # mid
  35. self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)
  36. # row
  37. self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)
  38. # foot_row
  39. (
  40. self.foot_row_left,
  41. self.foot_row_horizontal,
  42. self.foot_row_cross,
  43. self.foot_row_right,
  44. ) = iter(line6)
  45. # foot
  46. self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)
  47. # bottom
  48. self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(
  49. line8
  50. )
  51. def __repr__(self) -> str:
  52. return "Box(...)"
  53. def __str__(self) -> str:
  54. return self._box
  55. def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":
  56. """Substitute this box for another if it won't render due to platform issues.
  57. Args:
  58. options (ConsoleOptions): Console options used in rendering.
  59. safe (bool, optional): Substitute this for another Box if there are known problems
  60. displaying on the platform (currently only relevant on Windows). Default is True.
  61. Returns:
  62. Box: A different Box or the same Box.
  63. """
  64. box = self
  65. if options.legacy_windows and safe:
  66. box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
  67. if options.ascii_only and not box.ascii:
  68. box = ASCII
  69. return box
  70. def get_plain_headed_box(self) -> "Box":
  71. """If this box uses special characters for the borders of the header, then
  72. return the equivalent box that does not.
  73. Returns:
  74. Box: The most similar Box that doesn't use header-specific box characters.
  75. If the current Box already satisfies this criterion, then it's returned.
  76. """
  77. return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)
  78. def get_top(self, widths: Iterable[int]) -> str:
  79. """Get the top of a simple box.
  80. Args:
  81. widths (List[int]): Widths of columns.
  82. Returns:
  83. str: A string of box characters.
  84. """
  85. parts: List[str] = []
  86. append = parts.append
  87. append(self.top_left)
  88. for last, width in loop_last(widths):
  89. append(self.top * width)
  90. if not last:
  91. append(self.top_divider)
  92. append(self.top_right)
  93. return "".join(parts)
  94. def get_row(
  95. self,
  96. widths: Iterable[int],
  97. level: Literal["head", "row", "foot", "mid"] = "row",
  98. edge: bool = True,
  99. ) -> str:
  100. """Get the top of a simple box.
  101. Args:
  102. width (List[int]): Widths of columns.
  103. Returns:
  104. str: A string of box characters.
  105. """
  106. if level == "head":
  107. left = self.head_row_left
  108. horizontal = self.head_row_horizontal
  109. cross = self.head_row_cross
  110. right = self.head_row_right
  111. elif level == "row":
  112. left = self.row_left
  113. horizontal = self.row_horizontal
  114. cross = self.row_cross
  115. right = self.row_right
  116. elif level == "mid":
  117. left = self.mid_left
  118. horizontal = " "
  119. cross = self.mid_vertical
  120. right = self.mid_right
  121. elif level == "foot":
  122. left = self.foot_row_left
  123. horizontal = self.foot_row_horizontal
  124. cross = self.foot_row_cross
  125. right = self.foot_row_right
  126. else:
  127. raise ValueError("level must be 'head', 'row' or 'foot'")
  128. parts: List[str] = []
  129. append = parts.append
  130. if edge:
  131. append(left)
  132. for last, width in loop_last(widths):
  133. append(horizontal * width)
  134. if not last:
  135. append(cross)
  136. if edge:
  137. append(right)
  138. return "".join(parts)
  139. def get_bottom(self, widths: Iterable[int]) -> str:
  140. """Get the bottom of a simple box.
  141. Args:
  142. widths (List[int]): Widths of columns.
  143. Returns:
  144. str: A string of box characters.
  145. """
  146. parts: List[str] = []
  147. append = parts.append
  148. append(self.bottom_left)
  149. for last, width in loop_last(widths):
  150. append(self.bottom * width)
  151. if not last:
  152. append(self.bottom_divider)
  153. append(self.bottom_right)
  154. return "".join(parts)
  155. # fmt: off
  156. ASCII: Box = Box(
  157. "+--+\n"
  158. "| ||\n"
  159. "|-+|\n"
  160. "| ||\n"
  161. "|-+|\n"
  162. "|-+|\n"
  163. "| ||\n"
  164. "+--+\n",
  165. ascii=True,
  166. )
  167. ASCII2: Box = Box(
  168. "+-++\n"
  169. "| ||\n"
  170. "+-++\n"
  171. "| ||\n"
  172. "+-++\n"
  173. "+-++\n"
  174. "| ||\n"
  175. "+-++\n",
  176. ascii=True,
  177. )
  178. ASCII_DOUBLE_HEAD: Box = Box(
  179. "+-++\n"
  180. "| ||\n"
  181. "+=++\n"
  182. "| ||\n"
  183. "+-++\n"
  184. "+-++\n"
  185. "| ||\n"
  186. "+-++\n",
  187. ascii=True,
  188. )
  189. SQUARE: Box = Box(
  190. "┌─┬┐\n"
  191. "│ ││\n"
  192. "├─┼┤\n"
  193. "│ ││\n"
  194. "├─┼┤\n"
  195. "├─┼┤\n"
  196. "│ ││\n"
  197. "└─┴┘\n"
  198. )
  199. SQUARE_DOUBLE_HEAD: Box = Box(
  200. "┌─┬┐\n"
  201. "│ ││\n"
  202. "╞═╪╡\n"
  203. "│ ││\n"
  204. "├─┼┤\n"
  205. "├─┼┤\n"
  206. "│ ││\n"
  207. "└─┴┘\n"
  208. )
  209. MINIMAL: Box = Box(
  210. " ╷ \n"
  211. " │ \n"
  212. "╶─┼╴\n"
  213. " │ \n"
  214. "╶─┼╴\n"
  215. "╶─┼╴\n"
  216. " │ \n"
  217. " ╵ \n"
  218. )
  219. MINIMAL_HEAVY_HEAD: Box = Box(
  220. " ╷ \n"
  221. " │ \n"
  222. "╺━┿╸\n"
  223. " │ \n"
  224. "╶─┼╴\n"
  225. "╶─┼╴\n"
  226. " │ \n"
  227. " ╵ \n"
  228. )
  229. MINIMAL_DOUBLE_HEAD: Box = Box(
  230. " ╷ \n"
  231. " │ \n"
  232. " ═╪ \n"
  233. " │ \n"
  234. " ─┼ \n"
  235. " ─┼ \n"
  236. " │ \n"
  237. " ╵ \n"
  238. )
  239. SIMPLE: Box = Box(
  240. " \n"
  241. " \n"
  242. " ── \n"
  243. " \n"
  244. " \n"
  245. " ── \n"
  246. " \n"
  247. " \n"
  248. )
  249. SIMPLE_HEAD: Box = Box(
  250. " \n"
  251. " \n"
  252. " ── \n"
  253. " \n"
  254. " \n"
  255. " \n"
  256. " \n"
  257. " \n"
  258. )
  259. SIMPLE_HEAVY: Box = Box(
  260. " \n"
  261. " \n"
  262. " ━━ \n"
  263. " \n"
  264. " \n"
  265. " ━━ \n"
  266. " \n"
  267. " \n"
  268. )
  269. HORIZONTALS: Box = Box(
  270. " ── \n"
  271. " \n"
  272. " ── \n"
  273. " \n"
  274. " ── \n"
  275. " ── \n"
  276. " \n"
  277. " ── \n"
  278. )
  279. ROUNDED: Box = Box(
  280. "╭─┬╮\n"
  281. "│ ││\n"
  282. "├─┼┤\n"
  283. "│ ││\n"
  284. "├─┼┤\n"
  285. "├─┼┤\n"
  286. "│ ││\n"
  287. "╰─┴╯\n"
  288. )
  289. HEAVY: Box = Box(
  290. "┏━┳┓\n"
  291. "┃ ┃┃\n"
  292. "┣━╋┫\n"
  293. "┃ ┃┃\n"
  294. "┣━╋┫\n"
  295. "┣━╋┫\n"
  296. "┃ ┃┃\n"
  297. "┗━┻┛\n"
  298. )
  299. HEAVY_EDGE: Box = Box(
  300. "┏━┯┓\n"
  301. "┃ │┃\n"
  302. "┠─┼┨\n"
  303. "┃ │┃\n"
  304. "┠─┼┨\n"
  305. "┠─┼┨\n"
  306. "┃ │┃\n"
  307. "┗━┷┛\n"
  308. )
  309. HEAVY_HEAD: Box = Box(
  310. "┏━┳┓\n"
  311. "┃ ┃┃\n"
  312. "┡━╇┩\n"
  313. "│ ││\n"
  314. "├─┼┤\n"
  315. "├─┼┤\n"
  316. "│ ││\n"
  317. "└─┴┘\n"
  318. )
  319. DOUBLE: Box = Box(
  320. "╔═╦╗\n"
  321. "║ ║║\n"
  322. "╠═╬╣\n"
  323. "║ ║║\n"
  324. "╠═╬╣\n"
  325. "╠═╬╣\n"
  326. "║ ║║\n"
  327. "╚═╩╝\n"
  328. )
  329. DOUBLE_EDGE: Box = Box(
  330. "╔═╤╗\n"
  331. "║ │║\n"
  332. "╟─┼╢\n"
  333. "║ │║\n"
  334. "╟─┼╢\n"
  335. "╟─┼╢\n"
  336. "║ │║\n"
  337. "╚═╧╝\n"
  338. )
  339. MARKDOWN: Box = Box(
  340. " \n"
  341. "| ||\n"
  342. "|-||\n"
  343. "| ||\n"
  344. "|-||\n"
  345. "|-||\n"
  346. "| ||\n"
  347. " \n",
  348. ascii=True,
  349. )
  350. # fmt: on
  351. # Map Boxes that don't render with raster fonts on to equivalent that do
  352. LEGACY_WINDOWS_SUBSTITUTIONS = {
  353. ROUNDED: SQUARE,
  354. MINIMAL_HEAVY_HEAD: MINIMAL,
  355. SIMPLE_HEAVY: SIMPLE,
  356. HEAVY: SQUARE,
  357. HEAVY_EDGE: SQUARE,
  358. HEAVY_HEAD: SQUARE,
  359. }
  360. # Map headed boxes to their headerless equivalents
  361. PLAIN_HEADED_SUBSTITUTIONS = {
  362. HEAVY_HEAD: SQUARE,
  363. SQUARE_DOUBLE_HEAD: SQUARE,
  364. MINIMAL_DOUBLE_HEAD: MINIMAL,
  365. MINIMAL_HEAVY_HEAD: MINIMAL,
  366. ASCII_DOUBLE_HEAD: ASCII2,
  367. }
  368. if __name__ == "__main__": # pragma: no cover
  369. from rich.columns import Columns
  370. from rich.panel import Panel
  371. from . import box as box
  372. from .console import Console
  373. from .table import Table
  374. from .text import Text
  375. console = Console(record=True)
  376. BOXES = [
  377. "ASCII",
  378. "ASCII2",
  379. "ASCII_DOUBLE_HEAD",
  380. "SQUARE",
  381. "SQUARE_DOUBLE_HEAD",
  382. "MINIMAL",
  383. "MINIMAL_HEAVY_HEAD",
  384. "MINIMAL_DOUBLE_HEAD",
  385. "SIMPLE",
  386. "SIMPLE_HEAD",
  387. "SIMPLE_HEAVY",
  388. "HORIZONTALS",
  389. "ROUNDED",
  390. "HEAVY",
  391. "HEAVY_EDGE",
  392. "HEAVY_HEAD",
  393. "DOUBLE",
  394. "DOUBLE_EDGE",
  395. "MARKDOWN",
  396. ]
  397. console.print(Panel("[bold green]Box Constants", style="green"), justify="center")
  398. console.print()
  399. columns = Columns(expand=True, padding=2)
  400. for box_name in sorted(BOXES):
  401. table = Table(
  402. show_footer=True, style="dim", border_style="not dim", expand=True
  403. )
  404. table.add_column("Header 1", "Footer 1")
  405. table.add_column("Header 2", "Footer 2")
  406. table.add_row("Cell", "Cell")
  407. table.add_row("Cell", "Cell")
  408. table.box = getattr(box, box_name)
  409. table.title = Text(f"box.{box_name}", style="magenta")
  410. columns.add_renderable(table)
  411. console.print(columns)
  412. # console.save_svg("box.svg")