Skip to content

Commit

Permalink
spinner column
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Dec 5, 2020
1 parent 279b62b commit f018047
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 54 deletions.
2 changes: 1 addition & 1 deletion docs/source/console.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
2 changes: 1 addition & 1 deletion rich/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions rich/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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
Expand Down Expand Up @@ -38,7 +38,7 @@ def __init__(
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 [])
Expand Down
1 change: 1 addition & 0 deletions rich/default_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"progress.percentage": Style(color="magenta"),
"progress.remaining": Style(color="cyan"),
"progress.data.speed": Style(color="red"),
"progress.spinner": Style(color="green"),
}

MARKDOWN_STYLES = {
Expand Down
6 changes: 3 additions & 3 deletions rich/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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
Expand Down Expand Up @@ -39,7 +39,7 @@ def __init__(
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",
Expand All @@ -65,7 +65,7 @@ def fit(
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",
Expand Down
98 changes: 69 additions & 29 deletions rich/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
)

from . import filesize, get_console

from .console import (
Console,
ConsoleRenderable,
Expand All @@ -40,9 +39,10 @@
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)

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -143,6 +145,7 @@ def track(
transient=transient,
get_time=get_time,
refresh_per_second=refresh_per_second,
disable=disable,
)

with progress:
Expand Down Expand Up @@ -188,6 +191,38 @@ def render(self, task: "Task") -> RenderableType:
"""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."""

Expand Down Expand Up @@ -811,33 +846,30 @@ def advance(self, task_id: TaskID, advance: float = 1) -> None:

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

warnings.warn('install "ipywidgets" for Jupyter support')
else:
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:
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."""
Expand Down Expand Up @@ -990,7 +1022,15 @@ def process_renderables(

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)
Expand Down
36 changes: 24 additions & 12 deletions rich/spinner.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -43,20 +52,24 @@ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
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(
[
Expand All @@ -68,4 +81,3 @@ def render(self, time: float) -> Text:
with Live(all_spinners, refresh_per_second=20) as live:
while True:
sleep(0.1)
live.refresh()
17 changes: 11 additions & 6 deletions rich/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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
Expand Down Expand Up @@ -548,10 +548,16 @@ def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
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)

Expand All @@ -567,7 +573,6 @@ def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:
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
Expand Down Expand Up @@ -752,11 +757,11 @@ def pad_right(self, count: int, character: str = " ") -> None:
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 " ".
"""
Expand Down

0 comments on commit f018047

Please sign in to comment.