mirror of
https://github.com/Textualize/rich.git
synced 2025-08-21 02:41:11 +00:00
commit
f84d5dee6a
13 changed files with 207 additions and 32 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.3.0] - 2020-07-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added title and title_align options to Panel
|
||||||
|
- Added pad and width parameters to Align
|
||||||
|
- Added end parameter to Rule
|
||||||
|
- Added Text.pad and Text.align methods
|
||||||
|
- Added leading parameter to Table
|
||||||
|
|
||||||
## [3.2.0] - 2020-07-10
|
## [3.2.0] - 2020-07-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
name = "rich"
|
name = "rich"
|
||||||
homepage = "https://github.com/willmcgugan/rich"
|
homepage = "https://github.com/willmcgugan/rich"
|
||||||
documentation = "https://rich.readthedocs.io/en/latest/"
|
documentation = "https://rich.readthedocs.io/en/latest/"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
from .constrain import Constrain
|
||||||
from .jupyter import JupyterMixin
|
from .jupyter import JupyterMixin
|
||||||
from .measure import Measurement
|
from .measure import Measurement
|
||||||
from .segment import Segment
|
from .segment import Segment
|
||||||
|
@ -20,13 +21,21 @@ class Align(JupyterMixin):
|
||||||
renderable (RenderableType): A console renderable.
|
renderable (RenderableType): A console renderable.
|
||||||
align (AlignValues): One of "left", "center", or "right""
|
align (AlignValues): One of "left", "center", or "right""
|
||||||
style (StyleType, optional): An optional style to apply to the renderable.
|
style (StyleType, optional): An optional style to apply to the renderable.
|
||||||
|
pad (bool, optional): Pad the right with spaces. Defaults to True.
|
||||||
|
width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: if ``align`` is not one of the expected values.
|
ValueError: if ``align`` is not one of the expected values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, renderable: "RenderableType", align: AlignValues, style: StyleType = None
|
self,
|
||||||
|
renderable: "RenderableType",
|
||||||
|
align: AlignValues,
|
||||||
|
style: StyleType = None,
|
||||||
|
*,
|
||||||
|
pad: bool = True,
|
||||||
|
width: int = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if align not in ("left", "center", "right"):
|
if align not in ("left", "center", "right"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -35,35 +44,58 @@ class Align(JupyterMixin):
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.align = align
|
self.align = align
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.pad = pad
|
||||||
|
self.width = width
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def left(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
|
def left(
|
||||||
|
cls,
|
||||||
|
renderable: "RenderableType",
|
||||||
|
style: StyleType = None,
|
||||||
|
*,
|
||||||
|
pad: bool = True,
|
||||||
|
width: int = None,
|
||||||
|
) -> "Align":
|
||||||
"""Align a renderable to the left."""
|
"""Align a renderable to the left."""
|
||||||
return cls(renderable, "left", style=style)
|
return cls(renderable, "left", style=style, pad=pad, width=width)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def center(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
|
def center(
|
||||||
|
cls,
|
||||||
|
renderable: "RenderableType",
|
||||||
|
style: StyleType = None,
|
||||||
|
*,
|
||||||
|
pad: bool = True,
|
||||||
|
width: int = None,
|
||||||
|
) -> "Align":
|
||||||
"""Align a renderable to the center."""
|
"""Align a renderable to the center."""
|
||||||
return cls(renderable, "center", style=style)
|
return cls(renderable, "center", style=style, pad=pad, width=width)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def right(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
|
def right(
|
||||||
|
cls,
|
||||||
|
renderable: "RenderableType",
|
||||||
|
style: StyleType = None,
|
||||||
|
*,
|
||||||
|
pad: bool = True,
|
||||||
|
width: int = None,
|
||||||
|
) -> "Align":
|
||||||
"""Align a renderable to the right."""
|
"""Align a renderable to the right."""
|
||||||
return cls(renderable, "right", style=style)
|
return cls(renderable, "right", style=style, pad=pad, width=width)
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: "Console", options: "ConsoleOptions"
|
self, console: "Console", options: "ConsoleOptions"
|
||||||
) -> "RenderResult":
|
) -> "RenderResult":
|
||||||
|
|
||||||
align = self.align
|
align = self.align
|
||||||
rendered = console.render(self.renderable, options)
|
rendered = console.render(Constrain(self.renderable, width=self.width), options)
|
||||||
lines = list(Segment.split_lines(rendered))
|
lines = list(Segment.split_lines(rendered))
|
||||||
width, height = Segment.get_shape(lines)
|
width, height = Segment.get_shape(lines)
|
||||||
lines = Segment.set_shape(lines, width, height)
|
lines = Segment.set_shape(lines, width, height)
|
||||||
new_line = Segment.line()
|
new_line = Segment.line()
|
||||||
excess_space = options.max_width - width
|
excess_space = options.max_width - width
|
||||||
|
|
||||||
def generate_segments():
|
def generate_segments() -> Iterable[Segment]:
|
||||||
if excess_space <= 0:
|
if excess_space <= 0:
|
||||||
# Exact fit
|
# Exact fit
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
@ -72,17 +104,18 @@ class Align(JupyterMixin):
|
||||||
|
|
||||||
elif align == "left":
|
elif align == "left":
|
||||||
# Pad on the right
|
# Pad on the right
|
||||||
pad = Segment(" " * excess_space)
|
pad = Segment(" " * excess_space) if self.pad else None
|
||||||
for line in lines:
|
for line in lines:
|
||||||
yield from line
|
yield from line
|
||||||
yield pad
|
if pad:
|
||||||
|
yield pad
|
||||||
yield new_line
|
yield new_line
|
||||||
|
|
||||||
elif align == "center":
|
elif align == "center":
|
||||||
# Pad left and right
|
# Pad left and right
|
||||||
left = excess_space // 2
|
left = excess_space // 2
|
||||||
pad = Segment(" " * left)
|
pad = Segment(" " * left)
|
||||||
pad_right = Segment(" " * (excess_space - left))
|
pad_right = Segment(" " * (excess_space - left)) if self.pad else None
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if left:
|
if left:
|
||||||
yield pad
|
yield pad
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Box:
|
||||||
def get_row(
|
def get_row(
|
||||||
self,
|
self,
|
||||||
widths: Iterable[int],
|
widths: Iterable[int],
|
||||||
level: Literal["head", "row", "foot"] = "row",
|
level: Literal["head", "row", "foot", "mid"] = "row",
|
||||||
edge: bool = True,
|
edge: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get the top of a simple box.
|
"""Get the top of a simple box.
|
||||||
|
@ -102,6 +102,11 @@ class Box:
|
||||||
horizontal = self.row_horizontal
|
horizontal = self.row_horizontal
|
||||||
cross = self.row_cross
|
cross = self.row_cross
|
||||||
right = self.row_right
|
right = self.row_right
|
||||||
|
elif level == "mid":
|
||||||
|
left = self.mid_left
|
||||||
|
horizontal = " "
|
||||||
|
cross = self.mid_vertical
|
||||||
|
right = self.mid_right
|
||||||
elif level == "foot":
|
elif level == "foot":
|
||||||
left = self.foot_row_left
|
left = self.foot_row_left
|
||||||
horizontal = self.foot_row_horizontal
|
horizontal = self.foot_row_horizontal
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from typing import Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from .console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
||||||
from .jupyter import JupyterMixin
|
from .jupyter import JupyterMixin
|
||||||
from .measure import Measurement
|
from .measure import Measurement
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||||
|
|
||||||
|
|
||||||
class Constrain(JupyterMixin):
|
class Constrain(JupyterMixin):
|
||||||
"""Constrain the width of a renderable to a given number of characters.
|
"""Constrain the width of a renderable to a given number of characters.
|
||||||
|
@ -13,20 +15,20 @@ class Constrain(JupyterMixin):
|
||||||
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
|
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, renderable: RenderableType, width: Optional[int] = 80) -> None:
|
def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None:
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.width = width
|
self.width = width
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: "Console", options: "ConsoleOptions"
|
||||||
) -> RenderResult:
|
) -> "RenderResult":
|
||||||
if self.width is None:
|
if self.width is None:
|
||||||
yield self.renderable
|
yield self.renderable
|
||||||
else:
|
else:
|
||||||
child_options = options.update(width=min(self.width, options.max_width))
|
child_options = options.update(width=min(self.width, options.max_width))
|
||||||
yield from console.render(self.renderable, child_options)
|
yield from console.render(self.renderable, child_options)
|
||||||
|
|
||||||
def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
|
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
|
||||||
if self.width is None:
|
if self.width is None:
|
||||||
return Measurement.get(console, self.renderable, max_width)
|
return Measurement.get(console, self.renderable, max_width)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -2,6 +2,7 @@ from typing import Optional, Union
|
||||||
|
|
||||||
from .box import get_safe_box, Box, SQUARE, ROUNDED
|
from .box import get_safe_box, Box, SQUARE, ROUNDED
|
||||||
|
|
||||||
|
from .align import AlignValues
|
||||||
from .console import (
|
from .console import (
|
||||||
Console,
|
Console,
|
||||||
ConsoleOptions,
|
ConsoleOptions,
|
||||||
|
@ -12,7 +13,7 @@ from .console import (
|
||||||
from .jupyter import JupyterMixin
|
from .jupyter import JupyterMixin
|
||||||
from .padding import Padding, PaddingDimensions
|
from .padding import Padding, PaddingDimensions
|
||||||
from .style import Style, StyleType
|
from .style import Style, StyleType
|
||||||
from .text import Text
|
from .text import Text, TextType
|
||||||
from .segment import Segment
|
from .segment import Segment
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,6 +41,8 @@ class Panel(JupyterMixin):
|
||||||
renderable: RenderableType,
|
renderable: RenderableType,
|
||||||
box: Box = ROUNDED,
|
box: Box = ROUNDED,
|
||||||
*,
|
*,
|
||||||
|
title: TextType = None,
|
||||||
|
title_align: AlignValues = "center",
|
||||||
safe_box: Optional[bool] = None,
|
safe_box: Optional[bool] = None,
|
||||||
expand: bool = True,
|
expand: bool = True,
|
||||||
style: StyleType = "none",
|
style: StyleType = "none",
|
||||||
|
@ -49,6 +52,8 @@ class Panel(JupyterMixin):
|
||||||
) -> None:
|
) -> None:
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.box = box
|
self.box = box
|
||||||
|
self.title = title
|
||||||
|
self.title_align = title_align
|
||||||
self.safe_box = safe_box
|
self.safe_box = safe_box
|
||||||
self.expand = expand
|
self.expand = expand
|
||||||
self.style = style
|
self.style = style
|
||||||
|
@ -62,6 +67,8 @@ class Panel(JupyterMixin):
|
||||||
renderable: RenderableType,
|
renderable: RenderableType,
|
||||||
box: Box = ROUNDED,
|
box: Box = ROUNDED,
|
||||||
*,
|
*,
|
||||||
|
title: TextType = None,
|
||||||
|
title_align: AlignValues = "center",
|
||||||
safe_box: Optional[bool] = None,
|
safe_box: Optional[bool] = None,
|
||||||
style: StyleType = "none",
|
style: StyleType = "none",
|
||||||
border_style: StyleType = "none",
|
border_style: StyleType = "none",
|
||||||
|
@ -72,6 +79,8 @@ class Panel(JupyterMixin):
|
||||||
return cls(
|
return cls(
|
||||||
renderable,
|
renderable,
|
||||||
box,
|
box,
|
||||||
|
title=title,
|
||||||
|
title_align=title_align,
|
||||||
safe_box=safe_box,
|
safe_box=safe_box,
|
||||||
style=style,
|
style=style,
|
||||||
border_style=border_style,
|
border_style=border_style,
|
||||||
|
@ -108,7 +117,24 @@ class Panel(JupyterMixin):
|
||||||
line_start = Segment(box.mid_left, border_style)
|
line_start = Segment(box.mid_left, border_style)
|
||||||
line_end = Segment(f"{box.mid_right}", border_style)
|
line_end = Segment(f"{box.mid_right}", border_style)
|
||||||
new_line = Segment.line()
|
new_line = Segment.line()
|
||||||
yield Segment(box.get_top([width - 2]), border_style)
|
if self.title is None:
|
||||||
|
yield Segment(box.get_top([width - 2]), border_style)
|
||||||
|
else:
|
||||||
|
title_text = (
|
||||||
|
Text.from_markup(self.title)
|
||||||
|
if isinstance(self.title, str)
|
||||||
|
else self.title.copy()
|
||||||
|
)
|
||||||
|
title_text.style = border_style
|
||||||
|
title_text.end = ""
|
||||||
|
title_text.plain = title_text.plain.replace("\n", " ")
|
||||||
|
title_text = title_text.tabs_to_spaces()
|
||||||
|
title_text.pad(1)
|
||||||
|
title_text.align(self.title_align, width - 4, character=box.top)
|
||||||
|
yield Segment(box.top_left + box.top, border_style)
|
||||||
|
yield title_text
|
||||||
|
yield Segment(box.top + box.top_right, border_style)
|
||||||
|
|
||||||
yield new_line
|
yield new_line
|
||||||
for line in lines:
|
for line in lines:
|
||||||
yield line_start
|
yield line_start
|
||||||
|
@ -129,7 +155,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||||
c = Console()
|
c = Console()
|
||||||
|
|
||||||
from .padding import Padding
|
from .padding import Padding
|
||||||
from .box import ROUNDED
|
from .box import ROUNDED, DOUBLE
|
||||||
|
|
||||||
p = Panel(
|
p = Panel(
|
||||||
Panel.fit(
|
Panel.fit(
|
||||||
|
@ -138,7 +164,8 @@ if __name__ == "__main__": # pragma: no cover
|
||||||
safe_box=True,
|
safe_box=True,
|
||||||
style="on red",
|
style="on red",
|
||||||
),
|
),
|
||||||
border_style="on blue",
|
title="[b]Hello, World",
|
||||||
|
box=DOUBLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(p)
|
print(p)
|
||||||
|
|
|
@ -8,11 +8,13 @@ from .text import Text
|
||||||
|
|
||||||
|
|
||||||
class Rule(JupyterMixin):
|
class Rule(JupyterMixin):
|
||||||
"""A console renderable to draw a horizontal rule (line).
|
r"""A console renderable to draw a horizontal rule (line).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
|
title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
|
||||||
character (str, optional): Character used to draw the line. Defaults to "─".
|
character (str, optional): Character used to draw the line. Defaults to "─".
|
||||||
|
style (StyleType, optional): Style of Rule. Defaults to "rule.line".
|
||||||
|
end (str, optional): Character at end of Rule. defaults to "\\n"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -21,12 +23,14 @@ class Rule(JupyterMixin):
|
||||||
*,
|
*,
|
||||||
character: str = None,
|
character: str = None,
|
||||||
style: Union[str, Style] = "rule.line",
|
style: Union[str, Style] = "rule.line",
|
||||||
|
end: str = "\n",
|
||||||
) -> None:
|
) -> None:
|
||||||
if character and cell_len(character) != 1:
|
if character and cell_len(character) != 1:
|
||||||
raise ValueError("'character' argument must have a cell width of 1")
|
raise ValueError("'character' argument must have a cell width of 1")
|
||||||
self.title = title
|
self.title = title
|
||||||
self.character = character
|
self.character = character
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.end = end
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Rule({self.title!r}, {self.character!r})"
|
return f"Rule({self.title!r}, {self.character!r})"
|
||||||
|
@ -51,7 +55,7 @@ class Rule(JupyterMixin):
|
||||||
|
|
||||||
title_text.plain = title_text.plain.replace("\n", " ")
|
title_text.plain = title_text.plain.replace("\n", " ")
|
||||||
title_text = title_text.tabs_to_spaces()
|
title_text = title_text.tabs_to_spaces()
|
||||||
rule_text = Text()
|
rule_text = Text(end=self.end)
|
||||||
center = (width - cell_len(title_text.plain)) // 2
|
center = (width - cell_len(title_text.plain)) // 2
|
||||||
rule_text.append(character * (center - 1) + " ", self.style)
|
rule_text.append(character * (center - 1) + " ", self.style)
|
||||||
rule_text.append(title_text)
|
rule_text.append(title_text)
|
||||||
|
|
|
@ -109,6 +109,7 @@ class Table(JupyterMixin):
|
||||||
show_footer (bool, optional): Show a footer row. Defaults to False.
|
show_footer (bool, optional): Show a footer row. Defaults to False.
|
||||||
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
|
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
|
||||||
show_lines (bool, optional): Draw lines between every row. Defaults to False.
|
show_lines (bool, optional): Draw lines between every row. Defaults to False.
|
||||||
|
leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0.
|
||||||
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
|
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
|
||||||
row_styles (List[Union, str], optional): Optional list of row styles, if more that one style is give then the styles will alternate. Defaults to None.
|
row_styles (List[Union, str], optional): Optional list of row styles, if more that one style is give then the styles will alternate. Defaults to None.
|
||||||
header_style (Union[str, Style], optional): Style of the header. Defaults to None.
|
header_style (Union[str, Style], optional): Style of the header. Defaults to None.
|
||||||
|
@ -136,6 +137,7 @@ class Table(JupyterMixin):
|
||||||
show_footer: bool = False,
|
show_footer: bool = False,
|
||||||
show_edge: bool = True,
|
show_edge: bool = True,
|
||||||
show_lines: bool = False,
|
show_lines: bool = False,
|
||||||
|
leading: int = 0,
|
||||||
style: StyleType = "none",
|
style: StyleType = "none",
|
||||||
row_styles: Iterable[StyleType] = None,
|
row_styles: Iterable[StyleType] = None,
|
||||||
header_style: StyleType = None,
|
header_style: StyleType = None,
|
||||||
|
@ -161,6 +163,7 @@ class Table(JupyterMixin):
|
||||||
self.show_footer = show_footer
|
self.show_footer = show_footer
|
||||||
self.show_edge = show_edge
|
self.show_edge = show_edge
|
||||||
self.show_lines = show_lines
|
self.show_lines = show_lines
|
||||||
|
self.leading = leading
|
||||||
self.collapse_padding = collapse_padding
|
self.collapse_padding = collapse_padding
|
||||||
self.style = style
|
self.style = style
|
||||||
self.header_style = header_style
|
self.header_style = header_style
|
||||||
|
@ -593,6 +596,7 @@ class Table(JupyterMixin):
|
||||||
show_footer = self.show_footer
|
show_footer = self.show_footer
|
||||||
show_edge = self.show_edge
|
show_edge = self.show_edge
|
||||||
show_lines = self.show_lines
|
show_lines = self.show_lines
|
||||||
|
leading = self.leading
|
||||||
|
|
||||||
_Segment = Segment
|
_Segment = Segment
|
||||||
if _box:
|
if _box:
|
||||||
|
@ -685,15 +689,22 @@ class Table(JupyterMixin):
|
||||||
_box.get_row(widths, "head", edge=show_edge), border_style
|
_box.get_row(widths, "head", edge=show_edge), border_style
|
||||||
)
|
)
|
||||||
yield new_line
|
yield new_line
|
||||||
if _box and show_lines:
|
if _box and (show_lines or leading):
|
||||||
if (
|
if (
|
||||||
not last
|
not last
|
||||||
and not (show_footer and index >= len(rows) - 2)
|
and not (show_footer and index >= len(rows) - 2)
|
||||||
and not (show_header and header_row)
|
and not (show_header and header_row)
|
||||||
):
|
):
|
||||||
yield _Segment(
|
if leading:
|
||||||
_box.get_row(widths, "row", edge=show_edge), border_style
|
for _ in range(leading):
|
||||||
)
|
yield _Segment(
|
||||||
|
_box.get_row(widths, "mid", edge=show_edge),
|
||||||
|
border_style,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield _Segment(
|
||||||
|
_box.get_row(widths, "row", edge=show_edge), border_style
|
||||||
|
)
|
||||||
yield new_line
|
yield new_line
|
||||||
|
|
||||||
if _box and show_edge:
|
if _box and show_edge:
|
||||||
|
|
30
rich/text.py
30
rich/text.py
|
@ -16,6 +16,7 @@ from typing import (
|
||||||
|
|
||||||
from ._loop import loop_last
|
from ._loop import loop_last
|
||||||
from ._wrap import divide_line
|
from ._wrap import divide_line
|
||||||
|
from .align import AlignValues
|
||||||
from .cells import cell_len, set_cell_size
|
from .cells import cell_len, set_cell_size
|
||||||
from .containers import Lines
|
from .containers import Lines
|
||||||
from .control import strip_control_codes
|
from .control import strip_control_codes
|
||||||
|
@ -617,6 +618,15 @@ class Text(JupyterMixin):
|
||||||
append(span)
|
append(span)
|
||||||
self._spans[:] = spans
|
self._spans[:] = spans
|
||||||
|
|
||||||
|
def pad(self, count: int, character: str = " ") -> None:
|
||||||
|
"""Pad left and right with a given number of characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count (int): Width of padding.
|
||||||
|
"""
|
||||||
|
self.pad_left(count, character=character)
|
||||||
|
self.pad_right(count, character=character)
|
||||||
|
|
||||||
def pad_left(self, count: int, character: str = " ") -> None:
|
def pad_left(self, count: int, character: str = " ") -> None:
|
||||||
"""Pad the left with a given character.
|
"""Pad the left with a given character.
|
||||||
|
|
||||||
|
@ -640,6 +650,26 @@ class Text(JupyterMixin):
|
||||||
if count:
|
if count:
|
||||||
self.plain = f"{self.plain}{character * count}"
|
self.plain = f"{self.plain}{character * count}"
|
||||||
|
|
||||||
|
def align(self, align: AlignValues, width: int, character: str = " ") -> None:
|
||||||
|
"""Align text to a given width.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
align (AlignValues): One of "left", "center", or "right".
|
||||||
|
width (int): Desired width.
|
||||||
|
character (str, optional): Character to pad with. Defaults to " ".
|
||||||
|
"""
|
||||||
|
self.truncate(width)
|
||||||
|
excess_space = width - cell_len(self.plain)
|
||||||
|
if excess_space:
|
||||||
|
if align == "left":
|
||||||
|
self.pad_right(excess_space, character)
|
||||||
|
elif align == "center":
|
||||||
|
left = excess_space // 2
|
||||||
|
self.pad_left(left, character)
|
||||||
|
self.pad_right(excess_space - left, character)
|
||||||
|
else:
|
||||||
|
self.pad_left(excess_space, character)
|
||||||
|
|
||||||
def append(
|
def append(
|
||||||
self, text: Union["Text", str], style: Union[str, "Style"] = None
|
self, text: Union["Text", str], style: Union[str, "Style"] = None
|
||||||
) -> "Text":
|
) -> "Text":
|
||||||
|
|
|
@ -64,6 +64,22 @@ def test_measure():
|
||||||
assert _max == 7
|
assert _max == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_align_no_pad():
|
||||||
|
console = Console(file=io.StringIO(), width=10)
|
||||||
|
console.print(Align("foo", "center", pad=False))
|
||||||
|
console.print(Align("foo", "left", pad=False))
|
||||||
|
assert console.file.getvalue() == " foo\nfoo\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_align_width():
|
||||||
|
console = Console(file=io.StringIO(), width=40)
|
||||||
|
words = "Deep in the human unconscious is a pervasive need for a logical universe that makes sense. But the real universe is always one step beyond logic"
|
||||||
|
console.print(Align(words, "center", width=30))
|
||||||
|
result = console.file.getvalue()
|
||||||
|
expected = " Deep in the human unconscious \n is a pervasive need for a \n logical universe that makes \n sense. But the real universe \n is always one step beyond \n logic \n"
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
def test_shortcuts():
|
def test_shortcuts():
|
||||||
assert Align.left("foo").align == "left"
|
assert Align.left("foo").align == "left"
|
||||||
assert Align.left("foo").renderable == "foo"
|
assert Align.left("foo").renderable == "foo"
|
||||||
|
|
|
@ -10,6 +10,7 @@ tests = [
|
||||||
Panel.fit("Hello, World"),
|
Panel.fit("Hello, World"),
|
||||||
Panel("Hello, World", width=8),
|
Panel("Hello, World", width=8),
|
||||||
Panel(Panel("Hello, World")),
|
Panel(Panel("Hello, World")),
|
||||||
|
Panel("Hello, World", title="FOO"),
|
||||||
]
|
]
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
|
@ -18,6 +19,7 @@ expected = [
|
||||||
"╭────────────╮\n│Hello, World│\n╰────────────╯\n",
|
"╭────────────╮\n│Hello, World│\n╰────────────╯\n",
|
||||||
"╭──────╮\n│Hello,│\n│World │\n╰──────╯\n",
|
"╭──────╮\n│Hello,│\n│World │\n╰──────╯\n",
|
||||||
"╭────────────────────────────────────────────────╮\n│╭──────────────────────────────────────────────╮│\n││Hello, World ││\n│╰──────────────────────────────────────────────╯│\n╰────────────────────────────────────────────────╯\n",
|
"╭────────────────────────────────────────────────╮\n│╭──────────────────────────────────────────────╮│\n││Hello, World ││\n│╰──────────────────────────────────────────────╯│\n╰────────────────────────────────────────────────╯\n",
|
||||||
|
"╭───────────────────── FOO ──────────────────────╮\n│Hello, World │\n╰────────────────────────────────────────────────╯\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -517,3 +517,27 @@ def test_truncate_ellipsis_pad(input, count, expected):
|
||||||
text = Text(input)
|
text = Text(input)
|
||||||
text.truncate(count, overflow="ellipsis", pad=True)
|
text.truncate(count, overflow="ellipsis", pad=True)
|
||||||
assert text.plain == expected
|
assert text.plain == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_pad():
|
||||||
|
test = Text("foo")
|
||||||
|
test.pad(2)
|
||||||
|
assert test.plain == " foo "
|
||||||
|
|
||||||
|
|
||||||
|
def test_align_left():
|
||||||
|
test = Text("foo")
|
||||||
|
test.align("left", 10)
|
||||||
|
assert test.plain == "foo "
|
||||||
|
|
||||||
|
|
||||||
|
def test_align_right():
|
||||||
|
test = Text("foo")
|
||||||
|
test.align("right", 10)
|
||||||
|
assert test.plain == " foo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_align_center():
|
||||||
|
test = Text("foo")
|
||||||
|
test.align("center", 10)
|
||||||
|
assert test.plain == " foo "
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue