Browse Source

remove rich

Khaleel Al-Adhami 1 month ago
parent
commit
07754d6a66

+ 1 - 1
pyproject.toml

@@ -39,10 +39,10 @@ dependencies = [
   "python-socketio >=5.7.0,<6.0",
   "redis >=4.3.5,<6.0",
   "reflex-hosting-cli >=0.1.29",
-  "rich >=13.0.0,<14.0",
   "setuptools >=75.0",
   "starlette-admin >=0.11.0,<1.0",
   "sqlmodel >=0.0.14,<0.1",
+  "termcolor >=2.5.0,<2.6",
   "tomlkit >=0.12.4,<1.0",
   "twine >=4.0.0,<7.0",
   "typer >=0.15.1,<1.0",

+ 34 - 27
reflex/app.py

@@ -36,7 +36,6 @@ from fastapi import UploadFile as FastAPIUploadFile
 from fastapi.middleware import cors
 from fastapi.responses import JSONResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
-from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
 from socketio import ASGIApp, AsyncNamespace, AsyncServer
 from starlette.datastructures import Headers
 from starlette.datastructures import UploadFile as StarletteUploadFile
@@ -110,6 +109,13 @@ from reflex.utils import (
 )
 from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
 from reflex.utils.imports import ImportVar
+from reflex.utils.printer import (
+    CounterComponent,
+    FunGuyProgressComponent,
+    MessageComponent,
+    ProgressBar,
+    TimeComponent,
+)
 
 if TYPE_CHECKING:
     from reflex.vars import Var
@@ -1122,23 +1128,24 @@ class App(MiddlewareMixin, LifespanMixin):
 
             return
 
-        # Create a progress bar.
-        progress = Progress(
-            *Progress.get_default_columns()[:-1],
-            MofNCompleteColumn(),
-            TimeElapsedColumn(),
-        )
-
         # try to be somewhat accurate - but still not 100%
         adhoc_steps_without_executor = 7
         fixed_pages_within_executor = 5
-        progress.start()
-        task = progress.add_task(
-            f"[{get_compilation_time()}] Compiling:",
-            total=len(self._pages)
-            + (len(self._unevaluated_pages) * 2)
-            + fixed_pages_within_executor
-            + adhoc_steps_without_executor,
+
+        # Create a progress bar.
+        progress = ProgressBar(
+            steps=(
+                len(self._pages)
+                + (len(self._unevaluated_pages) * 2)
+                + fixed_pages_within_executor
+                + adhoc_steps_without_executor
+            ),
+            components=(
+                (MessageComponent(f"[{get_compilation_time()}] Compiling:"), 0),
+                (FunGuyProgressComponent(), 2),
+                (CounterComponent(), 1),
+                (TimeComponent(), 3),
+            ),
         )
 
         with console.timing("Evaluate Pages (Frontend)"):
@@ -1149,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 self._compile_page(route, save_page=should_compile)
                 end = timer()
                 performance_metrics.append((route, end - start))
-                progress.advance(task)
+                progress.update(1)
             console.debug(
                 "Slowest pages:\n"
                 + "\n".join(
@@ -1180,12 +1187,12 @@ class App(MiddlewareMixin, LifespanMixin):
         if is_prod_mode() and config.show_built_with_reflex:
             self._setup_sticky_badge()
 
-        progress.advance(task)
+        progress.update(1)
 
         # Store the compile results.
         compile_results: list[tuple[str, str]] = []
 
-        progress.advance(task)
+        progress.update(1)
 
         # Track imports and custom components found.
         all_imports = {}
@@ -1239,7 +1246,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 stateful_components_code,
                 page_components,
             ) = compiler.compile_stateful_components(self._pages.values())
-            progress.advance(task)
+            progress.update(1)
 
         # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
         if code_uses_state_contexts(stateful_components_code) and self._state is None:
@@ -1249,7 +1256,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
         compile_results.append((stateful_components_path, stateful_components_code))
 
-        progress.advance(task)
+        progress.update(1)
 
         # Compile the root document before fork.
         compile_results.append(
@@ -1262,7 +1269,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
         )
 
-        progress.advance(task)
+        progress.update(1)
 
         # Copy the assets.
         assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
@@ -1287,7 +1294,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
             def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs):
                 f = executor.submit(fn, *args, **kwargs)
-                f.add_done_callback(lambda _: progress.advance(task))
+                f.add_done_callback(lambda _: progress.update(1))
                 result_futures.append(f)
 
             # Compile the pre-compiled pages.
@@ -1323,7 +1330,7 @@ class App(MiddlewareMixin, LifespanMixin):
         # Get imports from AppWrap components.
         all_imports.update(app_root._get_all_imports())
 
-        progress.advance(task)
+        progress.update(1)
 
         # Compile the contexts.
         compile_results.append(
@@ -1332,13 +1339,13 @@ class App(MiddlewareMixin, LifespanMixin):
         if self.theme is not None:
             # Fix #2992 by removing the top-level appearance prop
             self.theme.appearance = None
-        progress.advance(task)
+        progress.update(1)
 
         # Compile the app root.
         compile_results.append(
             compiler.compile_app(app_root),
         )
-        progress.advance(task)
+        progress.update(1)
 
         # Compile custom components.
         (
@@ -1349,8 +1356,8 @@ class App(MiddlewareMixin, LifespanMixin):
         compile_results.append((custom_components_output, custom_components_result))
         all_imports.update(custom_components_imports)
 
-        progress.advance(task)
-        progress.stop()
+        progress.update(1)
+        progress.finish()
 
         # Install frontend packages.
         with console.timing("Install Frontend Packages"):

+ 8 - 8
reflex/custom_components/custom_components.py

@@ -338,7 +338,7 @@ def init(
     # Check the name follows the convention if picked.
     name_variants = _validate_library_name(library_name)
 
-    console.rule(f"[bold]Initializing {name_variants.package_name} project")
+    console.rule(f"Initializing {name_variants.package_name} project", bold=True)
 
     _populate_custom_component_project(name_variants)
 
@@ -351,25 +351,25 @@ def init(
 
     if install:
         package_name = name_variants.package_name
-        console.rule(f"[bold]Installing {package_name} in editable mode.")
+        console.rule(f"Installing {package_name} in editable mode.", bold=True)
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
         else:
             raise typer.Exit(code=1)
 
-    console.print("[bold]Custom component initialized successfully!")
-    console.rule("[bold]Project Summary")
+    console.success("Custom component initialized successfully!", bold=True)
+    console.rule("Project Summary", bold=True)
     console.print(
-        f"[ {CustomComponents.PACKAGE_README} ]: Package description. Please add usage examples."
+        f"[{CustomComponents.PACKAGE_README}]: Package description. Please add usage examples."
     )
     console.print(
-        f"[ {CustomComponents.PYPROJECT_TOML} ]: Project configuration file. Please fill in details such as your name, email, homepage URL."
+        f"[{CustomComponents.PYPROJECT_TOML}]: Project configuration file. Please fill in details such as your name, email, homepage URL."
     )
     console.print(
-        f"[ {CustomComponents.SRC_DIR}/ ]: Custom component code template. Start by editing it with your component implementation."
+        f"[{CustomComponents.SRC_DIR}/]: Custom component code template. Start by editing it with your component implementation."
     )
     console.print(
-        f"[ {name_variants.demo_app_dir}/ ]: Demo App. Add more code to this app and test."
+        f"[{name_variants.demo_app_dir}/]: Demo App. Add more code to this app and test."
     )
 
 

+ 2 - 2
reflex/reflex.py

@@ -79,7 +79,7 @@ def _init(
 
     # Validate the app name.
     app_name = prerequisites.validate_app_name(name)
-    console.rule(f"[bold]Initializing {app_name}")
+    console.rule(f"Initializing {app_name}", bold=True)
 
     # Check prerequisites.
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
@@ -200,7 +200,7 @@ def _run(
     # Reload the config to make sure the env vars are persistent.
     get_config(reload=True)
 
-    console.rule("[bold]Starting Reflex App")
+    console.rule("Starting Reflex App", bold=True)
 
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
 

+ 2 - 14
reflex/utils/build.py

@@ -8,8 +8,6 @@ import subprocess
 import zipfile
 from pathlib import Path
 
-from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
-
 from reflex import constants
 from reflex.config import get_config
 from reflex.utils import console, path_ops, prerequisites, processes
@@ -114,19 +112,9 @@ def _zip(
             ]
 
     # Create a progress bar for zipping the component.
-    progress = Progress(
-        *Progress.get_default_columns()[:-1],
-        MofNCompleteColumn(),
-        TimeElapsedColumn(),
-    )
-    task = progress.add_task(
-        f"Zipping {component_name.value}:", total=len(files_to_zip)
-    )
-
-    with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
+    console.info(f"Zipping {component_name.value} to {target}")
+    with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
         for file in files_to_zip:
-            console.debug(f"{target}: {file}", progress=progress)
-            progress.advance(task)
             zipf.write(file, Path(file).relative_to(root_dir))
 
 

+ 211 - 30
reflex/utils/console.py

@@ -3,19 +3,211 @@
 from __future__ import annotations
 
 import contextlib
+import dataclasses
 import inspect
 import os
+import re
 import shutil
+import sys
 import time
+import types
+from dataclasses import dataclass
 from pathlib import Path
 from types import FrameType
 
-from rich.console import Console
-from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
-from rich.prompt import Prompt
+from termcolor import colored
 
 from reflex.constants import LogLevel
 
+
+def _get_terminal_width() -> int:
+    try:
+        # First try using shutil, which is more reliable across platforms
+        return shutil.get_terminal_size().columns
+    except (AttributeError, ValueError, OSError):
+        try:
+            # Fallback to environment variables
+            return int(os.environ.get("COLUMNS", os.environ.get("TERM_WIDTH", 80)))
+        except (TypeError, ValueError):
+            # Default fallback
+            return 80
+
+
+@dataclasses.dataclass
+class Reprinter:
+    """A class that reprints text on the terminal."""
+
+    _text: str = dataclasses.field(default="", init=False)
+
+    @staticmethod
+    def _moveup(lines: int):
+        for _ in range(lines):
+            sys.stdout.write("\x1b[A")
+
+    def reprint(self, text: str):
+        """Reprint the text.
+
+        Args:
+            text: The text to print
+        """
+        if not text.endswith("\n"):
+            text += "\n"
+
+        # Clear previous text by overwritig non-spaces with spaces
+        self._moveup(self._text.count("\n"))
+        sys.stdout.write(re.sub(r"[^\s]", " ", self._text))
+
+        # Print new text
+        lines = min(self._text.count("\n"), text.count("\n"))
+        self._moveup(lines)
+        sys.stdout.write(text)
+        sys.stdout.flush()
+        self._text = text
+
+    def finish(self):
+        """Finish printing the text."""
+        self._moveup(1)
+
+
+@dataclass
+class Status:
+    """A status class for displaying a spinner."""
+
+    message: str = "Loading"
+    _reprinter: Reprinter | None = None
+
+    def __enter__(self):
+        """Enter the context manager.
+
+        Returns:
+            The status object.
+        """
+        self._reprinter = Reprinter()
+        return self
+
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: types.TracebackType | None,
+    ):
+        """Exit the context manager.
+
+        Args:
+            exc_type: The exception type.
+            exc_value: The exception value.
+            traceback: The traceback.
+        """
+        if self._reprinter:
+            self._reprinter.reprint("")
+            self._reprinter.finish()
+            self._reprinter = None
+
+    def update(self, msg: str, **kwargs):
+        """Update the status spinner.
+
+        Args:
+            msg: The message to display.
+            kwargs: Keyword arguments to pass to the print function.
+        """
+        if self._reprinter:
+            self._reprinter.reprint(f"O {msg}")
+
+
+@dataclass
+class Console:
+    """A console class for pretty printing."""
+
+    def print(self, msg: str, **kwargs):
+        """Print a message.
+
+        Args:
+            msg: The message to print.
+            kwargs: Keyword arguments to pass to the print function.
+        """
+        from builtins import print
+
+        color = kwargs.pop("color", None)
+        bold = kwargs.pop("bold", False)
+        if color or bold:
+            msg = colored(msg, color, attrs=["bold"] if bold else [])
+
+        print(msg, **kwargs)  # noqa: T201
+
+    def rule(self, title: str, **kwargs):
+        """Prints a horizontal rule with a title.
+
+        Args:
+            title: The title of the rule.
+            kwargs: Keyword arguments to pass to the print function.
+        """
+        terminal_width = _get_terminal_width()
+        remaining_width = (
+            terminal_width - len(title) - 4
+        )  # 4 for the spaces and characters around the title
+        left_padding = remaining_width // 2
+        right_padding = remaining_width - left_padding
+
+        rule_line = "─" * left_padding + f" {title} " + "─" * right_padding
+        self.print(rule_line, **kwargs)
+
+    def status(self, *args, **kwargs):
+        """Create a status.
+
+        Args:
+            *args: Args to pass to the status.
+            **kwargs: Kwargs to pass to the status.
+
+        Returns:
+            A new status.
+        """
+        return Status(*args, **kwargs)
+
+
+class Prompt:
+    """A class for prompting the user for input."""
+
+    @staticmethod
+    def ask(
+        question: str,
+        choices: list[str] | None = None,
+        default: str | None = None,
+        show_choices: bool = True,
+    ) -> str | None:
+        """Ask the user a question.
+
+        Args:
+            question: The question to ask the user.
+            choices: A list of choices to select from.
+            default: The default option selected.
+            show_choices: Whether to show the choices.
+
+        Returns:
+            The user's response or the default value.
+        """
+        prompt = question
+
+        if choices and show_choices:
+            choice_str = "/".join(choices)
+            prompt = f"{question} [{choice_str}]"
+
+        if default is not None:
+            prompt = f"{prompt} ({default})"
+
+        prompt = f"{prompt}: "
+
+        response = input(prompt)
+
+        if not response and default is not None:
+            return default
+
+        if choices and response not in choices:
+            print(f"Please choose from: {', '.join(choices)}")
+            return Prompt.ask(question, choices, default, show_choices)
+
+        return response
+
+
 # Console for pretty printing.
 _console = Console()
 
@@ -101,16 +293,13 @@ def debug(msg: str, dedupe: bool = False, **kwargs):
         kwargs: Keyword arguments to pass to the print function.
     """
     if is_debug():
-        msg_ = f"[purple]Debug: {msg}[/purple]"
         if dedupe:
-            if msg_ in _EMITTED_DEBUG:
+            if msg in _EMITTED_DEBUG:
                 return
             else:
-                _EMITTED_DEBUG.add(msg_)
-        if progress := kwargs.pop("progress", None):
-            progress.console.print(msg_, **kwargs)
-        else:
-            print(msg_, **kwargs)
+                _EMITTED_DEBUG.add(msg)
+        kwargs.setdefault("color", "purple")
+        print(msg, **kwargs)
 
 
 def info(msg: str, dedupe: bool = False, **kwargs):
@@ -127,7 +316,8 @@ def info(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMITTED_INFO.add(msg)
-        print(f"[cyan]Info: {msg}[/cyan]", **kwargs)
+        kwargs.setdefault("color", "cyan")
+        print(f"Info: {msg}", **kwargs)
 
 
 def success(msg: str, dedupe: bool = False, **kwargs):
@@ -144,7 +334,8 @@ def success(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMITTED_SUCCESS.add(msg)
-        print(f"[green]Success: {msg}[/green]", **kwargs)
+        kwargs.setdefault("color", "green")
+        print(f"Success: {msg}", **kwargs)
 
 
 def log(msg: str, dedupe: bool = False, **kwargs):
@@ -161,7 +352,7 @@ def log(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMITTED_LOGS.add(msg)
-        _console.log(msg, **kwargs)
+        _console.print(msg, **kwargs)
 
 
 def rule(title: str, **kwargs):
@@ -188,7 +379,8 @@ def warn(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMIITED_WARNINGS.add(msg)
-        print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
+        kwargs.setdefault("color", "light_yellow")
+        print(f"Warning: {msg}", **kwargs)
 
 
 def _get_first_non_framework_frame() -> FrameType | None:
@@ -254,7 +446,8 @@ def deprecate(
             f"removed in {removal_version}. ({loc})"
         )
         if _LOG_LEVEL <= LogLevel.WARNING:
-            print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
+            kwargs.setdefault("color", "yellow")
+            print(f"DeprecationWarning: {msg}", **kwargs)
         if dedupe:
             _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key)
 
@@ -273,7 +466,8 @@ def error(msg: str, dedupe: bool = False, **kwargs):
                 return
             else:
                 _EMITTED_ERRORS.add(msg)
-        print(f"[red]{msg}[/red]", **kwargs)
+        kwargs.setdefault("color", "red")
+        print(f"{msg}", **kwargs)
 
 
 def ask(
@@ -299,19 +493,6 @@ def ask(
     )
 
 
-def progress():
-    """Create a new progress bar.
-
-    Returns:
-        A new progress bar.
-    """
-    return Progress(
-        *Progress.get_default_columns()[:-1],
-        MofNCompleteColumn(),
-        TimeElapsedColumn(),
-    )
-
-
 def status(*args, **kwargs):
     """Create a status with a spinner.
 
@@ -339,4 +520,4 @@ def timing(msg: str):
     try:
         yield
     finally:
-        debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]")
+        debug(f"[timing] {msg}: {time.time() - start:.2f}s", color="white")

+ 2 - 2
reflex/utils/exec.py

@@ -153,7 +153,7 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
     prerequisites.validate_frontend_dependencies(init=False)
 
     # Run the frontend in development mode.
-    console.rule("[bold green]App Running")
+    console.rule("App Running", color="green", bold=True)
     os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
     run_process_and_launch_url(
         [
@@ -180,7 +180,7 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
     # validate dependencies before run
     prerequisites.validate_frontend_dependencies(init=False)
     # Run the frontend in production mode.
-    console.rule("[bold green]App Running")
+    console.rule("App Running", color="green", bold=True)
     run_process_and_launch_url(
         [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
         backend_present,

+ 1 - 1
reflex/utils/export.py

@@ -51,7 +51,7 @@ def export(
     exec.output_system_info()
 
     # Compile the app in production mode and export it.
-    console.rule("[bold]Compiling production app and preparing for export.")
+    console.rule("Compiling production app and preparing for export.", bold=True)
 
     if frontend:
         # Ensure module can be imported and app.compile() is called.

+ 387 - 0
reflex/utils/printer.py

@@ -0,0 +1,387 @@
+"""A module that provides a progress bar for the terminal."""
+
+import dataclasses
+import time
+from typing import Protocol, Sequence
+
+from reflex.utils.console import Reprinter, _get_terminal_width
+
+reprinter = Reprinter()
+
+
+class ProgressBarComponent(Protocol):
+    """A protocol for progress bar components."""
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        ...
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        ...
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+        ...
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        ...
+
+
+@dataclasses.dataclass
+class MessageComponent(ProgressBarComponent):
+    """A simple component that displays a message."""
+
+    message: str
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        return len(self.message)
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        return len(self.message)
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        return self.message
+
+
+@dataclasses.dataclass
+class PercentageComponent(ProgressBarComponent):
+    """A component that displays the percentage of completion."""
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        return 4
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        return 4
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        return f"{int(current / steps * 100):3}%"
+
+
+@dataclasses.dataclass
+class TimeComponent(ProgressBarComponent):
+    """A component that displays the time elapsed."""
+
+    initial_time: float | None = None
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        if self.initial_time is None:
+            raise ValueError("TimeComponent not initialized")
+        return len(f"{time.time() - self.initial_time:.1f}s")
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        if self.initial_time is None:
+            raise ValueError("TimeComponent not initialized")
+        return len(f"{time.time() - self.initial_time:.1f}s")
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+        self.initial_time = time.time()
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        if self.initial_time is None:
+            raise ValueError("TimeComponent not initialized")
+        return f"{time.time() - self.initial_time:.1f}s"
+
+
+@dataclasses.dataclass
+class CounterComponent(ProgressBarComponent):
+    """A component that displays the current step and total steps."""
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        return 1 + 2 * len(str(steps))
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        return 1 + 2 * len(str(steps))
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        return current.__format__(f"{len(str(steps))}") + "/" + str(steps)
+
+
+@dataclasses.dataclass
+class SimpleProgressComponent:
+    """A component that displays a not so fun guy."""
+
+    starting_str: str = ""
+    ending_str: str = ""
+    complete_str: str = "█"
+    incomplete_str: str = "░"
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        return (
+            len(self.starting_str)
+            + 2 * len(self.incomplete_str)
+            + 2 * len(self.complete_str)
+            + len(self.ending_str)
+        )
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        return (
+            len(self.starting_str)
+            + steps * max(len(self.incomplete_str), len(self.complete_str))
+            + len(self.ending_str)
+        )
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        progress = int(
+            current
+            / steps
+            * (max_width - len(self.starting_str) - len(self.ending_str))
+        )
+
+        complete_part = self.complete_str * (progress // len(self.complete_str))
+
+        incomplete_part = self.incomplete_str * (
+            (
+                max_width
+                - len(self.starting_str)
+                - len(self.ending_str)
+                - len(complete_part)
+            )
+            // len(self.incomplete_str)
+        )
+
+        return self.starting_str + complete_part + incomplete_part + self.ending_str
+
+
+@dataclasses.dataclass
+class FunGuyProgressComponent:
+    """A component that displays a fun guy."""
+
+    starting_str: str = ""
+    ending_str: str = ""
+    fun_guy_running: Sequence[str] = ("🯇", "🯈")
+    fun_guy_finished: str = "🯆"
+    incomplete_str: str = "·"
+    complete_str: str = " "
+
+    def minimum_width(self, current: int, steps: int) -> int:
+        """Return the minimum width of the component."""
+        return (
+            len(self.starting_str)
+            + len(self.incomplete_str)
+            + 1
+            + len(self.complete_str)
+            + len(self.ending_str)
+        )
+
+    def requested_width(self, current: int, steps: int) -> int:
+        """Return the requested width of the component."""
+        return steps + len(self.starting_str) + len(self.ending_str)
+
+    def initialize(self, steps: int) -> None:
+        """Initialize the component."""
+
+    def get_message(self, current: int, steps: int, max_width: int) -> str:
+        """Return the message to display."""
+        progress = int(
+            current
+            / steps
+            * (max_width - len(self.starting_str) - len(self.ending_str))
+        )
+        fun_guy = (
+            self.fun_guy_running[progress % len(self.fun_guy_running)]
+            if current != steps
+            else self.fun_guy_finished
+        )
+
+        before_guy = self.complete_str * max(0, progress - len(fun_guy))
+        after_guy = self.incomplete_str * max(
+            0,
+            max_width
+            - len(before_guy)
+            - len(fun_guy)
+            - len(self.starting_str)
+            - len(self.ending_str),
+        )
+        return self.starting_str + before_guy + fun_guy + after_guy + self.ending_str
+
+
+@dataclasses.dataclass
+class ProgressBar:
+    """A progress bar that displays the progress of a task."""
+
+    steps: int
+    max_width: int = 80
+    separator: str = " "
+    components: Sequence[tuple[ProgressBarComponent, int]] = dataclasses.field(
+        default_factory=lambda: [
+            (FunGuyProgressComponent(), 2),
+            (CounterComponent(), 3),
+            (PercentageComponent(), 0),
+            (TimeComponent(), 1),
+        ]
+    )
+
+    _printer: Reprinter = dataclasses.field(default_factory=Reprinter, init=False)
+    _current: int = dataclasses.field(default=0, init=False)
+
+    def __post_init__(self):
+        """Initialize the progress bar."""
+        for component, _ in self.components:
+            component.initialize(self.steps)
+
+    def print(self):
+        """Print the current progress bar state."""
+        current_terminal_width = _get_terminal_width()
+
+        components_by_priority = [
+            (index, component)
+            for index, (component, _) in sorted(
+                enumerate(self.components), key=lambda x: x[1][1], reverse=True
+            )
+        ]
+
+        possible_width = min(current_terminal_width, self.max_width)
+        sum_of_minimum_widths = sum(
+            component.minimum_width(self._current, self.steps)
+            for _, component in components_by_priority
+        )
+
+        if sum_of_minimum_widths > possible_width:
+            used_width = 0
+
+            visible_components: list[tuple[int, ProgressBarComponent, int]] = []
+
+            for index, component in components_by_priority:
+                if (
+                    used_width
+                    + component.minimum_width(self._current, self.steps)
+                    + len(self.separator)
+                    > possible_width
+                ):
+                    continue
+
+                used_width += component.minimum_width(self._current, self.steps)
+                visible_components.append(
+                    (
+                        index,
+                        component,
+                        component.requested_width(self._current, self.steps),
+                    )
+                )
+        else:
+            components = [
+                (
+                    priority,
+                    component,
+                    component.minimum_width(self._current, self.steps),
+                )
+                for (component, priority) in self.components
+            ]
+
+            while True:
+                sum_of_assigned_width = sum(width for _, _, width in components)
+
+                extra_width = (
+                    possible_width
+                    - sum_of_assigned_width
+                    - (len(self.separator) * (len(components) - 1))
+                )
+
+                possible_extra_width_to_take = [
+                    (
+                        max(
+                            0,
+                            component.requested_width(self._current, self.steps)
+                            - width,
+                        ),
+                        priority,
+                    )
+                    for priority, component, width in components
+                ]
+
+                sum_of_possible_extra_width = sum(
+                    width for width, _ in possible_extra_width_to_take
+                )
+
+                if sum_of_possible_extra_width <= 0 or extra_width <= 0:
+                    break
+
+                min_width, max_prioririty = min(
+                    filter(lambda x: x[0] > 0, possible_extra_width_to_take),
+                    key=lambda x: x[0] / x[1],
+                )
+
+                maximum_prioririty_repeats = min_width / max_prioririty
+
+                give_width = [
+                    min(width, maximum_prioririty_repeats * priority)
+                    for width, priority in possible_extra_width_to_take
+                ]
+                sum_of_give_width = sum(give_width)
+
+                normalized_give_width = [
+                    width / sum_of_give_width * min(extra_width, sum_of_give_width)
+                    for width in give_width
+                ]
+
+                components = [
+                    (index, component, int(width + give))
+                    for (index, component, width), give in zip(
+                        components, normalized_give_width, strict=True
+                    )
+                ]
+
+                if sum(width for _, _, width in components) == sum_of_minimum_widths:
+                    break
+
+            visible_components = [
+                (index, component, width)
+                for index, (_, component, width) in enumerate(components)
+                if width > 0
+            ]
+
+        messages = [
+            self.get_message(component, width)
+            for _, component, width in sorted(visible_components, key=lambda x: x[0])
+        ]
+
+        self._printer.reprint(self.separator.join(messages))
+
+    def get_message(self, component: ProgressBarComponent, width: int):
+        """Get the message for a given component."""
+        message = component.get_message(self._current, self.steps, width)
+        if len(message) > width:
+            raise ValueError(
+                f"Component message too long: {message} (length: {len(message)}, width: {width})"
+            )
+        return message
+
+    def update(self, step: int):
+        """Update the progress bar by a given step."""
+        self._current += step
+        self.print()
+
+    def finish(self):
+        """Finish the progress bar."""
+        self._current = self.steps
+        self.print()

+ 24 - 15
reflex/utils/processes.py

@@ -15,11 +15,16 @@ from typing import Any, Callable, Generator, Literal, Sequence, Tuple, overload
 import psutil
 import typer
 from redis.exceptions import RedisError
-from rich.progress import Progress
 
 from reflex import constants
 from reflex.config import environment
 from reflex.utils import console, path_ops, prerequisites
+from reflex.utils.printer import (
+    CounterComponent,
+    FunGuyProgressComponent,
+    MessageComponent,
+    ProgressBar,
+)
 
 
 def kill(pid: int):
@@ -273,7 +278,6 @@ def run_concurrently(*fns: Callable | Tuple) -> None:
 def stream_logs(
     message: str,
     process: subprocess.Popen,
-    progress: Progress | None = None,
     suppress_errors: bool = False,
     analytics_enabled: bool = False,
 ):
@@ -282,7 +286,6 @@ def stream_logs(
     Args:
         message: The message to display.
         process: The process.
-        progress: The ongoing progress bar if one is being used.
         suppress_errors: If True, do not exit if errors are encountered (for fallback).
         analytics_enabled: Whether analytics are enabled for this command.
 
@@ -297,11 +300,11 @@ def stream_logs(
     # Store the tail of the logs.
     logs = collections.deque(maxlen=512)
     with process:
-        console.debug(message, progress=progress)
+        console.debug(message)
         if process.stdout is None:
             return
         for line in process.stdout:
-            console.debug(line, end="", progress=progress)
+            console.debug(line, end="")
             logs.append(line)
             yield line
 
@@ -367,16 +370,22 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: list[str
         checkpoints: The checkpoints to advance the progress bar.
     """
     # Iterate over the process output.
-    with console.progress() as progress:
-        task = progress.add_task(f"{message}: ", total=len(checkpoints))
-        for line in stream_logs(message, process, progress=progress):
-            # Check for special strings and update the progress bar.
-            for special_string in checkpoints:
-                if special_string in line:
-                    progress.update(task, advance=1)
-                    if special_string == checkpoints[-1]:
-                        progress.update(task, completed=len(checkpoints))
-                    break
+    progress = ProgressBar(
+        steps=len(checkpoints),
+        components=(
+            (MessageComponent(message), 0),
+            (FunGuyProgressComponent(), 2),
+            (CounterComponent(), 1),
+        ),
+    )
+    for line in stream_logs(message, process):
+        # Check for special strings and update the progress bar.
+        for special_string in checkpoints:
+            if special_string in line:
+                progress.update(1)
+                if special_string == checkpoints[-1]:
+                    progress.finish()
+                break
 
 
 def atexit_handler():

+ 12 - 3
uv.lock

@@ -640,7 +640,7 @@ name = "importlib-metadata"
 version = "8.6.1"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
-    { name = "zipp" },
+    { name = "zipp", marker = "python_full_version < '3.12'" },
 ]
 sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
 wheels = [
@@ -1731,10 +1731,10 @@ dependencies = [
     { name = "python-socketio" },
     { name = "redis" },
     { name = "reflex-hosting-cli" },
-    { name = "rich" },
     { name = "setuptools" },
     { name = "sqlmodel" },
     { name = "starlette-admin" },
+    { name = "termcolor" },
     { name = "tomlkit" },
     { name = "twine" },
     { name = "typer" },
@@ -1794,10 +1794,10 @@ requires-dist = [
     { name = "python-socketio", specifier = ">=5.7.0,<6.0" },
     { name = "redis", specifier = ">=4.3.5,<6.0" },
     { name = "reflex-hosting-cli", specifier = ">=0.1.29" },
-    { name = "rich", specifier = ">=13.0.0,<14.0" },
     { name = "setuptools", specifier = ">=75.0" },
     { name = "sqlmodel", specifier = ">=0.0.14,<0.1" },
     { name = "starlette-admin", specifier = ">=0.11.0,<1.0" },
+    { name = "termcolor", specifier = ">=2.5.0,<2.6" },
     { name = "tomlkit", specifier = ">=0.12.4,<1.0" },
     { name = "twine", specifier = ">=4.0.0,<7.0" },
     { name = "typer", specifier = ">=0.15.1,<1.0" },
@@ -2110,6 +2110,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 },
 ]
 
+[[package]]
+name = "termcolor"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/72/88311445fd44c455c7d553e61f95412cf89054308a1aa2434ab835075fc5/termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f", size = 13057 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 },
+]
+
 [[package]]
 name = "text-unidecode"
 version = "1.3"