mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 01:58:24 +00:00
spinner column
This commit is contained in:
parent
279b62b932
commit
f018047c25
8 changed files with 111 additions and 53 deletions
|
@ -81,7 +81,7 @@ The :meth:`~rich.console.Console.rule` method will draw a horizontal line with a
|
|||
|
||||
<pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="color: #00ff00">─────────────────────────────── </span><span style="color: #800000; font-weight: bold">Chapter 2</span><span style="color: #00ff00"> ───────────────────────────────</span></pre>
|
||||
|
||||
The rule method also accepts a `style` parameter to set the style of the line, and an `align` parameter to align the title ("left", "center", or "right").
|
||||
The rule method also accepts a ``style`` parameter to set the style of the line, and an ``align`` parameter to align the title ("left", "center", or "right").
|
||||
|
||||
Low level output
|
||||
----------------
|
||||
|
|
|
@ -20,7 +20,7 @@ class Align(JupyterMixin):
|
|||
|
||||
Args:
|
||||
renderable (RenderableType): A console renderable.
|
||||
align (AlignValues): One of "left", "center", or "right""
|
||||
align (AlignMethod): One of "left", "center", or "right""
|
||||
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.
|
||||
|
|
|
@ -3,7 +3,7 @@ from itertools import chain
|
|||
from operator import itemgetter
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from .align import Align, AlignValues
|
||||
from .align import Align, AlignMethod
|
||||
from .console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||
from .constrain import Constrain
|
||||
from .measure import Measurement
|
||||
|
@ -38,7 +38,7 @@ class Columns(JupyterMixin):
|
|||
equal: bool = False,
|
||||
column_first: bool = False,
|
||||
right_to_left: bool = False,
|
||||
align: AlignValues = None,
|
||||
align: AlignMethod = None,
|
||||
title: TextType = None,
|
||||
) -> None:
|
||||
self.renderables = list(renderables or [])
|
||||
|
|
|
@ -116,6 +116,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
|
|||
"progress.percentage": Style(color="magenta"),
|
||||
"progress.remaining": Style(color="cyan"),
|
||||
"progress.data.speed": Style(color="red"),
|
||||
"progress.spinner": Style(color="green"),
|
||||
}
|
||||
|
||||
MARKDOWN_STYLES = {
|
||||
|
|
|
@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
|
|||
|
||||
from .box import Box, ROUNDED
|
||||
|
||||
from .align import AlignValues
|
||||
from .align import AlignMethod
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement, measure_renderables
|
||||
from .padding import Padding, PaddingDimensions
|
||||
|
@ -39,7 +39,7 @@ class Panel(JupyterMixin):
|
|||
box: Box = ROUNDED,
|
||||
*,
|
||||
title: TextType = None,
|
||||
title_align: AlignValues = "center",
|
||||
title_align: AlignMethod = "center",
|
||||
safe_box: Optional[bool] = None,
|
||||
expand: bool = True,
|
||||
style: StyleType = "none",
|
||||
|
@ -65,7 +65,7 @@ class Panel(JupyterMixin):
|
|||
box: Box = ROUNDED,
|
||||
*,
|
||||
title: TextType = None,
|
||||
title_align: AlignValues = "center",
|
||||
title_align: AlignMethod = "center",
|
||||
safe_box: Optional[bool] = None,
|
||||
style: StyleType = "none",
|
||||
border_style: StyleType = "none",
|
||||
|
|
|
@ -25,7 +25,6 @@ from typing import (
|
|||
)
|
||||
|
||||
from . import filesize, get_console
|
||||
|
||||
from .console import (
|
||||
Console,
|
||||
ConsoleRenderable,
|
||||
|
@ -40,9 +39,10 @@ from .highlighter import Highlighter
|
|||
from .jupyter import JupyterMixin
|
||||
from .live_render import LiveRender
|
||||
from .progress_bar import ProgressBar
|
||||
from .spinner import Spinner
|
||||
from .style import StyleType
|
||||
from .table import Table
|
||||
from .text import Text
|
||||
from .text import Text, TextType
|
||||
|
||||
TaskID = NewType("TaskID", int)
|
||||
|
||||
|
@ -100,6 +100,7 @@ def track(
|
|||
finished_style: StyleType = "bar.finished",
|
||||
pulse_style: StyleType = "bar.pulse",
|
||||
update_period: float = 0.1,
|
||||
disable: bool = False,
|
||||
) -> Iterable[ProgressType]:
|
||||
"""Track progress by iterating over a sequence.
|
||||
|
||||
|
@ -116,6 +117,7 @@ def track(
|
|||
finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
|
||||
pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
|
||||
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
|
||||
disable (bool, optional): Disable display of progress.
|
||||
Returns:
|
||||
Iterable[ProgressType]: An iterable of the values in the sequence.
|
||||
|
||||
|
@ -143,6 +145,7 @@ def track(
|
|||
transient=transient,
|
||||
get_time=get_time,
|
||||
refresh_per_second=refresh_per_second,
|
||||
disable=disable,
|
||||
)
|
||||
|
||||
with progress:
|
||||
|
@ -188,6 +191,38 @@ class ProgressColumn(ABC):
|
|||
"""Should return a renderable object."""
|
||||
|
||||
|
||||
class SpinnerColumn(ProgressColumn):
|
||||
"""A column with a 'spinner' animation.
|
||||
|
||||
Args:
|
||||
spinner_name (str, optional): Name of spinner animation. Defaults to "dots".
|
||||
style (StyleType, optional): Style of spinner. Defaults to "progress.spinner".
|
||||
speed (float, optional): Speed faxtor of spinner. Defaults to 1.0.
|
||||
finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
spinner_name: str = "dots",
|
||||
style: StyleType = "progress.spinner",
|
||||
speed: float = 1.0,
|
||||
finished_text: TextType = " ",
|
||||
):
|
||||
self.spinner = Spinner(spinner_name, style=style, speed=speed)
|
||||
self.finished_text = (
|
||||
Text.from_markup(finished_text)
|
||||
if isinstance(finished_text, str)
|
||||
else finished_text
|
||||
)
|
||||
super().__init__()
|
||||
|
||||
def render(self, task: "Task") -> Text:
|
||||
if task.finished:
|
||||
return self.finished_text
|
||||
text = self.spinner.render(task._get_time())
|
||||
return text
|
||||
|
||||
|
||||
class TextColumn(ProgressColumn):
|
||||
"""A column containing text."""
|
||||
|
||||
|
@ -811,33 +846,30 @@ class Progress(JupyterMixin, RenderHook):
|
|||
|
||||
def refresh(self) -> None:
|
||||
"""Refresh (render) the progress information."""
|
||||
if self.console.is_jupyter: # pragma: no cover
|
||||
try:
|
||||
from IPython.display import display
|
||||
from ipywidgets import Output
|
||||
except ImportError:
|
||||
import warnings
|
||||
if not self.disable:
|
||||
if self.console.is_jupyter: # pragma: no cover
|
||||
try:
|
||||
from IPython.display import display
|
||||
from ipywidgets import Output
|
||||
except ImportError:
|
||||
import warnings
|
||||
|
||||
warnings.warn('install "ipywidgets" for Jupyter support')
|
||||
else:
|
||||
warnings.warn('install "ipywidgets" for Jupyter support')
|
||||
else:
|
||||
with self._lock:
|
||||
if self.ipy_widget is None:
|
||||
self.ipy_widget = Output()
|
||||
display(self.ipy_widget)
|
||||
|
||||
with self.ipy_widget:
|
||||
self.ipy_widget.clear_output(wait=True)
|
||||
self.console.print(self.get_renderable())
|
||||
|
||||
elif self.console.is_terminal and not self.console.is_dumb_terminal:
|
||||
with self._lock:
|
||||
if self.ipy_widget is None:
|
||||
self.ipy_widget = Output()
|
||||
display(self.ipy_widget)
|
||||
|
||||
with self.ipy_widget:
|
||||
self.ipy_widget.clear_output(wait=True)
|
||||
self.console.print(self.get_renderable())
|
||||
|
||||
elif (
|
||||
self.console.is_terminal
|
||||
and not self.console.is_dumb_terminal
|
||||
and not self.disable
|
||||
):
|
||||
with self._lock:
|
||||
self._live_render.set_renderable(self.get_renderable())
|
||||
with self.console:
|
||||
self.console.print(Control(""))
|
||||
self._live_render.set_renderable(self.get_renderable())
|
||||
with self.console:
|
||||
self.console.print(Control(""))
|
||||
|
||||
def get_renderable(self) -> RenderableType:
|
||||
"""Get a renderable for the progress display."""
|
||||
|
@ -990,7 +1022,15 @@ if __name__ == "__main__": # pragma: no coverage
|
|||
|
||||
console = Console(record=True)
|
||||
try:
|
||||
with Progress(console=console, transient=True) as progress:
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
TimeRemainingColumn(),
|
||||
console=console,
|
||||
transient=True,
|
||||
) as progress:
|
||||
|
||||
task1 = progress.add_task("[red]Downloading", total=1000)
|
||||
task2 = progress.add_task("[green]Processing", total=1000)
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from ._spinners import SPINNERS
|
||||
from .console import Console
|
||||
from .measure import Measurement
|
||||
from .style import StyleType
|
||||
from .text import Text, TextType
|
||||
from ._spinners import SPINNERS
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .console import Console, ConsoleOptions, RenderResult
|
||||
|
||||
|
||||
class Spinner:
|
||||
"""Base class for a spinner."""
|
||||
|
||||
def __init__(
|
||||
self, name: str, text: TextType = "", style: StyleType = None, speed=1.0
|
||||
self, name: str, text: TextType = "", *, style: StyleType = None, speed=1.0
|
||||
) -> None:
|
||||
"""A spinner animation.
|
||||
|
||||
Args:
|
||||
name (str): Name of spinner (run python -m rich.spinner).
|
||||
text (TextType, optional): Text to display at the right of the spinner. Defaults to "".
|
||||
style (StyleType, optional): Style for sinner amimation. Defaults to None.
|
||||
speed (float, optional): Speed factor for animation. Defaults to 1.0.
|
||||
|
||||
Raises:
|
||||
KeyError: If name isn't one of the supported spinner animations.
|
||||
"""
|
||||
try:
|
||||
spinner = SPINNERS[name]
|
||||
except KeyError:
|
||||
|
@ -43,20 +52,24 @@ class Spinner:
|
|||
return Measurement.get(console, text, max_width)
|
||||
|
||||
def render(self, time: float) -> Text:
|
||||
frame_no = int((time * self.speed) / (self.interval / 1000.0)) % len(
|
||||
self.frames
|
||||
)
|
||||
frame = Text(self.frames[frame_no])
|
||||
if self.style is not None:
|
||||
frame.stylize(self.style)
|
||||
"""Render the spinner for a given time.
|
||||
|
||||
Args:
|
||||
time (float): Time in seconds.
|
||||
|
||||
Returns:
|
||||
Text: A Text instance containing animation frame.
|
||||
"""
|
||||
frame_no = int((time * self.speed) / (self.interval / 1000.0))
|
||||
frame = Text(self.frames[frame_no % len(self.frames)], style=self.style or "")
|
||||
return Text.assemble(frame, " ", self.text) if self.text else frame
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from .live import Live
|
||||
from time import sleep
|
||||
|
||||
from .columns import Columns
|
||||
from .live import Live
|
||||
|
||||
all_spinners = Columns(
|
||||
[
|
||||
|
@ -68,4 +81,3 @@ if __name__ == "__main__": # pragma: no cover
|
|||
with Live(all_spinners, refresh_per_second=20) as live:
|
||||
while True:
|
||||
sleep(0.1)
|
||||
live.refresh()
|
||||
|
|
17
rich/text.py
17
rich/text.py
|
@ -20,7 +20,7 @@ from typing import (
|
|||
from ._loop import loop_last
|
||||
from ._pick import pick_bool
|
||||
from ._wrap import divide_line
|
||||
from .align import AlignValues
|
||||
from .align import AlignMethod
|
||||
from .cells import cell_len, set_cell_size
|
||||
from .containers import Lines
|
||||
from .control import strip_control_codes
|
||||
|
@ -548,10 +548,16 @@ class Text(JupyterMixin):
|
|||
Iterable[Segment]: Result of render that may be written to the console.
|
||||
"""
|
||||
|
||||
_Segment = Segment
|
||||
if not self._spans:
|
||||
yield _Segment(self.plain)
|
||||
if self.end:
|
||||
yield _Segment(end)
|
||||
return
|
||||
|
||||
text = self.plain
|
||||
null_style = Style.null()
|
||||
enumerated_spans = list(enumerate(self._spans, 1))
|
||||
get_style = partial(console.get_style, default=null_style)
|
||||
get_style = partial(console.get_style, default=Style.null())
|
||||
style_map = {index: get_style(span.style) for index, span in enumerated_spans}
|
||||
style_map[0] = get_style(self.style)
|
||||
|
||||
|
@ -567,7 +573,6 @@ class Text(JupyterMixin):
|
|||
stack_append = stack.append
|
||||
stack_pop = stack.remove
|
||||
|
||||
_Segment = Segment
|
||||
style_cache: Dict[Tuple[Style, ...], Style] = {}
|
||||
style_cache_get = style_cache.get
|
||||
combine = Style.combine
|
||||
|
@ -752,11 +757,11 @@ class Text(JupyterMixin):
|
|||
if count:
|
||||
self.plain = f"{self.plain}{character * count}"
|
||||
|
||||
def align(self, align: AlignValues, width: int, character: str = " ") -> None:
|
||||
def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
|
||||
"""Align text to a given width.
|
||||
|
||||
Args:
|
||||
align (AlignValues): One of "left", "center", or "right".
|
||||
align (AlignMethod): One of "left", "center", or "right".
|
||||
width (int): Desired width.
|
||||
character (str, optional): Character to pad with. Defaults to " ".
|
||||
"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue