Skip to content

Feature/indeterminate progress with elapsed #3710

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions examples/indeterminate_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Example usage of indeterminate progress bar with elapsed time."""

import time
from rich.console import Console
from rich.progress import (
Progress,
TextColumn,
BarColumn,
TimeElapsedColumn,
IndeterminateTaskProgressColumn,
)


def main():
"""Demonstrate the indeterminate progress bar as requested in issue #3572."""
console = Console()

console.print("[bold]Indeterminate Progress Bar Example[/bold]\n")
console.print("As requested in issue #3572: showing indeterminate state with elapsed time\n")

with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
"[progress.percentage]{task.percentage:>3.0f}%",
TimeElapsedColumn(),
IndeterminateTaskProgressColumn(),
console=console,
refresh_per_second=10,
) as progress:
# Create an indeterminate task with expected total
task_id = progress.add_task(
"Indexing",
indeterminate=True,
expected_total=10
)

console.print("Task is running in indeterminate mode...\n")

# Let it run for a few seconds showing the animation
for i in range(30):
time.sleep(0.1)
if i % 10 == 0:
console.print(f"Still working... ({i // 10 + 1}/3)")

# Now convert to determinate and complete
console.print("\nConverting to determinate and completing...")
progress.update(
task_id,
indeterminate=False,
total=10,
completed=10
)

# Keep it displayed for a moment
time.sleep(1)

console.print("\n[bold green]Task completed![/bold green]")


if __name__ == "__main__":
main()
92 changes: 92 additions & 0 deletions examples/visual_demo_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Visual demonstration of the indeterminate progress bar feature."""

import time
from rich.console import Console
from rich.progress import (
Progress,
TextColumn,
BarColumn,
TimeElapsedColumn,
IndeterminateTaskProgressColumn,
TaskProgressColumn,
)
from rich.panel import Panel
from rich.layout import Layout


def visual_demo():
"""A comprehensive visual demonstration of indeterminate progress bars."""
console = Console()

console.print("[bold blue]Indeterminate Progress Bar Demonstration[/bold blue]\n")
console.print("This demo shows the new features for issue #3572:")
console.print("- Indeterminate progress with animated pulse")
console.print("- Elapsed time display during indeterminate state")
console.print("- ?/total indicator for indeterminate tasks")
console.print("- Transition from indeterminate to determinate state\n")

with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
TimeElapsedColumn(),
IndeterminateTaskProgressColumn(),
console=console,
refresh_per_second=20, # Higher refresh rate for smoother animation
) as progress:

# Regular progress bar for comparison
regular_task = progress.add_task("[green]Regular task", total=100)

# Indeterminate task with expected total
indeterminate_task1 = progress.add_task(
"[yellow]Indexing",
indeterminate=True,
expected_total=10
)

# Indeterminate task without expected total
indeterminate_task2 = progress.add_task(
"[cyan]Searching",
indeterminate=True
)

# Simulate work
for i in range(100):
# Update regular task
progress.update(regular_task, advance=1)

# Let indeterminate tasks animate
time.sleep(0.05)

# Show message periodically
if i % 20 == 0 and i > 0:
console.print(f"[dim]Still processing... ({i}% of demo complete)[/dim]")

# Convert indeterminate tasks to determinate and complete them
console.print("\n[bold]Converting indeterminate tasks to determinate...[/bold]")

# Complete the indexing task
progress.update(
indeterminate_task1,
indeterminate=False,
total=10,
completed=10
)

# Complete the searching task
progress.update(
indeterminate_task2,
indeterminate=False,
total=1,
completed=1
)

time.sleep(1) # Show completed state

console.print("\n[bold green]Demo complete![/bold green]")
console.print("All tasks successfully transitioned from indeterminate to complete state.")


if __name__ == "__main__":
visual_demo()
76 changes: 75 additions & 1 deletion rich/markdown.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
import re
from typing import ClassVar, Iterable

from markdown_it import MarkdownIt
Expand All @@ -25,6 +26,7 @@
from .style import Style, StyleStack
from .syntax import Syntax
from .text import Text, TextType
from .math_render import MathRenderer


class MarkdownElement:
Expand Down Expand Up @@ -513,6 +515,8 @@ class Markdown(JupyterMixin):
enabled. Defaults to None.
inline_code_theme: (Optional[str], optional): Pygments theme for inline code
highlighting, or None for no highlighting. Defaults to None.
math_enabled (bool, optional): Enable math rendering. Defaults to True.
math_style (Style, optional): Style for math expressions. Defaults to None.
"""

elements: ClassVar[dict[str, type[MarkdownElement]]] = {
Expand Down Expand Up @@ -545,6 +549,8 @@ def __init__(
hyperlinks: bool = True,
inline_code_lexer: str | None = None,
inline_code_theme: str | None = None,
math_enabled: bool = True,
math_style: Style | None = None,
) -> None:
parser = MarkdownIt().enable("strikethrough").enable("table")
self.markup = markup
Expand All @@ -556,6 +562,11 @@ def __init__(
self.inline_code_lexer = inline_code_lexer
self.inline_code_theme = inline_code_theme or code_theme

# Math rendering support
self.math_enabled = math_enabled
self.math_style = math_style or Style(italic=True)
self.math_renderer = MathRenderer() if math_enabled else None

def _flatten_tokens(self, tokens: Iterable[Token]) -> Iterable[Token]:
"""Flattens the token stream."""
for token in tokens:
Expand All @@ -565,11 +576,74 @@ def _flatten_tokens(self, tokens: Iterable[Token]) -> Iterable[Token]:
yield from self._flatten_tokens(token.children)
else:
yield token

def _process_math_expressions(self, markup: str) -> str:
"""Process LaTeX math expressions in the text.

Args:
markup: Text that may contain math expressions

Returns:
Text with math expressions processed for rendering
"""
if not self.math_enabled or self.math_renderer is None:
return markup

# Track positions where math expressions are found to replace later
replacements = []

# Process block math ($$...$$)
block_pattern = re.compile(r'\$\$(.*?)\$\$', re.DOTALL)

for match in block_pattern.finditer(markup):
expression = match.group(1).strip()
rendered_text = self.math_renderer.render_to_text(expression, self.math_style)
replacements.append((match.start(), match.end(), rendered_text, True))

# Process inline math ($...$) - avoid $ used for money
inline_pattern = re.compile(r'\$([^\s$][^$]*?[^\s$])\$')

for match in inline_pattern.finditer(markup):
expression = match.group(1)
rendered_text = self.math_renderer.render_to_text(expression, self.math_style)
replacements.append((match.start(), match.end(), rendered_text, False))

# Apply replacements in reverse order to maintain positions
if not replacements:
return markup

result = []
last_end = 0

# Sort replacements by position
replacements.sort(key=lambda x: x[0])

for start, end, rendered_text, is_block in replacements:
result.append(markup[last_end:start])

if is_block:
result.append("\n\n")
result.append(str(rendered_text))
result.append("\n\n")
else:
result.append(str(rendered_text))

last_end = end

result.append(markup[last_end:])
return "".join(result)

def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
"""Render markdown to the console."""
# Process math expressions if enabled
if self.math_enabled and self.math_renderer is not None:
self.markup = self._process_math_expressions(self.markup)
# Re-parse the markup after math processing
parser = MarkdownIt().enable("strikethrough").enable("table")
self.parsed = parser.parse(self.markup)

style = console.get_style(self.style, default="none")
options = options.update(height=None)
context = MarkdownContext(
Expand Down Expand Up @@ -781,4 +855,4 @@ def __rich_console__(
console = Console(
force_terminal=args.force_color, width=args.width, record=True
)
console.print(markdown)
console.print(markdown)
Loading