codekingpro/portable-devtools
114k
1from typing import TYPE_CHECKING, Optional2 3from .align import AlignMethod4from .box import ROUNDED, Box5from .cells import cell_len6from .jupyter import JupyterMixin7from .measure import Measurement, measure_renderables8from .padding import Padding, PaddingDimensions9from .segment import Segment10from .style import Style, StyleType11from .text import Text, TextType12 13if TYPE_CHECKING:14 from .console import Console, ConsoleOptions, RenderableType, RenderResult15 16 17class Panel(JupyterMixin):18 """A console renderable that draws a border around its contents.19 20 Example:21 >>> console.print(Panel("Hello, World!"))22 23 Args:24 renderable (RenderableType): A console renderable object.25 box (Box): A Box instance that defines the look of the border (see :ref:`appendix_box`. Defaults to box.ROUNDED.26 title (Optional[TextType], optional): Optional title displayed in panel header. Defaults to None.27 title_align (AlignMethod, optional): Alignment of title. Defaults to "center".28 subtitle (Optional[TextType], optional): Optional subtitle displayed in panel footer. Defaults to None.29 subtitle_align (AlignMethod, optional): Alignment of subtitle. Defaults to "center".30 safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.31 expand (bool, optional): If True the panel will stretch to fill the console width, otherwise it will be sized to fit the contents. Defaults to True.32 style (str, optional): The style of the panel (border and contents). Defaults to "none".33 border_style (str, optional): The style of the border. Defaults to "none".34 width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.35 height (Optional[int], optional): Optional height of panel. Defaults to None to auto-detect.36 padding (Optional[PaddingDimensions]): Optional padding around renderable. Defaults to 0.37 highlight (bool, optional): Enable automatic highlighting of panel title (if str). Defaults to False.38 """39 40 def __init__(41 self,42 renderable: "RenderableType",43 box: Box = ROUNDED,44 *,45 title: Optional[TextType] = None,46 title_align: AlignMethod = "center",47 subtitle: Optional[TextType] = None,48 subtitle_align: AlignMethod = "center",49 safe_box: Optional[bool] = None,50 expand: bool = True,51 style: StyleType = "none",52 border_style: StyleType = "none",53 width: Optional[int] = None,54 height: Optional[int] = None,55 padding: PaddingDimensions = (0, 1),56 highlight: bool = False,57 ) -> None:58 self.renderable = renderable59 self.box = box60 self.title = title61 self.title_align: AlignMethod = title_align62 self.subtitle = subtitle63 self.subtitle_align = subtitle_align64 self.safe_box = safe_box65 self.expand = expand66 self.style = style67 self.border_style = border_style68 self.width = width69 self.height = height70 self.padding = padding71 self.highlight = highlight72 73 @classmethod74 def fit(75 cls,76 renderable: "RenderableType",77 box: Box = ROUNDED,78 *,79 title: Optional[TextType] = None,80 title_align: AlignMethod = "center",81 subtitle: Optional[TextType] = None,82 subtitle_align: AlignMethod = "center",83 safe_box: Optional[bool] = None,84 style: StyleType = "none",85 border_style: StyleType = "none",86 width: Optional[int] = None,87 height: Optional[int] = None,88 padding: PaddingDimensions = (0, 1),89 highlight: bool = False,90 ) -> "Panel":91 """An alternative constructor that sets expand=False."""92 return cls(93 renderable,94 box,95 title=title,96 title_align=title_align,97 subtitle=subtitle,98 subtitle_align=subtitle_align,99 safe_box=safe_box,100 style=style,101 border_style=border_style,102 width=width,103 height=height,104 padding=padding,105 highlight=highlight,106 expand=False,107 )108 109 @property110 def _title(self) -> Optional[Text]:111 if self.title:112 title_text = (113 Text.from_markup(self.title)114 if isinstance(self.title, str)115 else self.title.copy()116 )117 title_text.end = ""118 title_text.plain = title_text.plain.replace("\n", " ")119 title_text.no_wrap = True120 title_text.expand_tabs()121 title_text.pad(1)122 return title_text123 return None124 125 @property126 def _subtitle(self) -> Optional[Text]:127 if self.subtitle:128 subtitle_text = (129 Text.from_markup(self.subtitle)130 if isinstance(self.subtitle, str)131 else self.subtitle.copy()132 )133 subtitle_text.end = ""134 subtitle_text.plain = subtitle_text.plain.replace("\n", " ")135 subtitle_text.no_wrap = True136 subtitle_text.expand_tabs()137 subtitle_text.pad(1)138 return subtitle_text139 return None140 141 def __rich_console__(142 self, console: "Console", options: "ConsoleOptions"143 ) -> "RenderResult":144 _padding = Padding.unpack(self.padding)145 renderable = (146 Padding(self.renderable, _padding) if any(_padding) else self.renderable147 )148 style = console.get_style(self.style)149 border_style = style + console.get_style(self.border_style)150 width = (151 options.max_width152 if self.width is None153 else min(options.max_width, self.width)154 )155 156 safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box157 box = self.box.substitute(options, safe=safe_box)158 159 def align_text(160 text: Text, width: int, align: str, character: str, style: Style161 ) -> Text:162 """Gets new aligned text.163 164 Args:165 text (Text): Title or subtitle text.166 width (int): Desired width.167 align (str): Alignment.168 character (str): Character for alignment.169 style (Style): Border style170 171 Returns:172 Text: New text instance173 """174 text = text.copy()175 text.truncate(width)176 excess_space = width - cell_len(text.plain)177 if text.style:178 text.stylize(console.get_style(text.style))179 180 if excess_space:181 if align == "left":182 return Text.assemble(183 text,184 (character * excess_space, style),185 no_wrap=True,186 end="",187 )188 elif align == "center":189 left = excess_space // 2190 return Text.assemble(191 (character * left, style),192 text,193 (character * (excess_space - left), style),194 no_wrap=True,195 end="",196 )197 else:198 return Text.assemble(199 (character * excess_space, style),200 text,201 no_wrap=True,202 end="",203 )204 return text205 206 title_text = self._title207 if title_text is not None:208 title_text.stylize_before(border_style)209 210 child_width = (211 width - 2212 if self.expand213 else console.measure(214 renderable, options=options.update_width(width - 2)215 ).maximum216 )217 child_height = self.height or options.height or None218 if child_height:219 child_height -= 2220 if title_text is not None:221 child_width = min(222 options.max_width - 2, max(child_width, title_text.cell_len + 2)223 )224 225 width = child_width + 2226 child_options = options.update(227 width=child_width, height=child_height, highlight=self.highlight228 )229 lines = console.render_lines(renderable, child_options, style=style)230 231 line_start = Segment(box.mid_left, border_style)232 line_end = Segment(f"{box.mid_right}", border_style)233 new_line = Segment.line()234 if title_text is None or width <= 4:235 yield Segment(box.get_top([width - 2]), border_style)236 else:237 title_text = align_text(238 title_text,239 width - 4,240 self.title_align,241 box.top,242 border_style,243 )244 yield Segment(box.top_left + box.top, border_style)245 yield from console.render(title_text, child_options.update_width(width - 4))246 yield Segment(box.top + box.top_right, border_style)247 248 yield new_line249 for line in lines:250 yield line_start251 yield from line252 yield line_end253 yield new_line254 255 subtitle_text = self._subtitle256 if subtitle_text is not None:257 subtitle_text.stylize_before(border_style)258 259 if subtitle_text is None or width <= 4:260 yield Segment(box.get_bottom([width - 2]), border_style)261 else:262 subtitle_text = align_text(263 subtitle_text,264 width - 4,265 self.subtitle_align,266 box.bottom,267 border_style,268 )269 yield Segment(box.bottom_left + box.bottom, border_style)270 yield from console.render(271 subtitle_text, child_options.update_width(width - 4)272 )273 yield Segment(box.bottom + box.bottom_right, border_style)274 275 yield new_line276 277 def __rich_measure__(278 self, console: "Console", options: "ConsoleOptions"279 ) -> "Measurement":280 _title = self._title281 _, right, _, left = Padding.unpack(self.padding)282 padding = left + right283 renderables = [self.renderable, _title] if _title else [self.renderable]284 285 if self.width is None:286 width = (287 measure_renderables(288 console,289 options.update_width(options.max_width - padding - 2),290 renderables,291 ).maximum292 + padding293 + 2294 )295 else:296 width = self.width297 return Measurement(width, width)298 299 300if __name__ == "__main__": # pragma: no cover301 from .console import Console302 303 c = Console()304 305 from .box import DOUBLE, ROUNDED306 from .padding import Padding307 308 p = Panel(309 "Hello, World!",310 title="rich.Panel",311 style="white on blue",312 box=DOUBLE,313 padding=1,314 )315 316 c.print()317 c.print(p)318 