Khaleel Al-Adhami 1 ヶ月 前
コミット
07754d6a66

+ 1 - 1
pyproject.toml

@@ -39,10 +39,10 @@ dependencies = [
   "python-socketio >=5.7.0,<6.0",
   "python-socketio >=5.7.0,<6.0",
   "redis >=4.3.5,<6.0",
   "redis >=4.3.5,<6.0",
   "reflex-hosting-cli >=0.1.29",
   "reflex-hosting-cli >=0.1.29",
-  "rich >=13.0.0,<14.0",
   "setuptools >=75.0",
   "setuptools >=75.0",
   "starlette-admin >=0.11.0,<1.0",
   "starlette-admin >=0.11.0,<1.0",
   "sqlmodel >=0.0.14,<0.1",
   "sqlmodel >=0.0.14,<0.1",
+  "termcolor >=2.5.0,<2.6",
   "tomlkit >=0.12.4,<1.0",
   "tomlkit >=0.12.4,<1.0",
   "twine >=4.0.0,<7.0",
   "twine >=4.0.0,<7.0",
   "typer >=0.15.1,<1.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.middleware import cors
 from fastapi.responses import JSONResponse, StreamingResponse
 from fastapi.responses import JSONResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
-from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
 from socketio import ASGIApp, AsyncNamespace, AsyncServer
 from socketio import ASGIApp, AsyncNamespace, AsyncServer
 from starlette.datastructures import Headers
 from starlette.datastructures import Headers
 from starlette.datastructures import UploadFile as StarletteUploadFile
 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.exec import get_compile_context, is_prod_mode, is_testing_env
 from reflex.utils.imports import ImportVar
 from reflex.utils.imports import ImportVar
+from reflex.utils.printer import (
+    CounterComponent,
+    FunGuyProgressComponent,
+    MessageComponent,
+    ProgressBar,
+    TimeComponent,
+)
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from reflex.vars import Var
     from reflex.vars import Var
@@ -1122,23 +1128,24 @@ class App(MiddlewareMixin, LifespanMixin):
 
 
             return
             return
 
 
-        # Create a progress bar.
-        progress = Progress(
-            *Progress.get_default_columns()[:-1],
-            MofNCompleteColumn(),
-            TimeElapsedColumn(),
-        )
-
         # try to be somewhat accurate - but still not 100%
         # try to be somewhat accurate - but still not 100%
         adhoc_steps_without_executor = 7
         adhoc_steps_without_executor = 7
         fixed_pages_within_executor = 5
         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)"):
         with console.timing("Evaluate Pages (Frontend)"):
@@ -1149,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 self._compile_page(route, save_page=should_compile)
                 self._compile_page(route, save_page=should_compile)
                 end = timer()
                 end = timer()
                 performance_metrics.append((route, end - start))
                 performance_metrics.append((route, end - start))
-                progress.advance(task)
+                progress.update(1)
             console.debug(
             console.debug(
                 "Slowest pages:\n"
                 "Slowest pages:\n"
                 + "\n".join(
                 + "\n".join(
@@ -1180,12 +1187,12 @@ class App(MiddlewareMixin, LifespanMixin):
         if is_prod_mode() and config.show_built_with_reflex:
         if is_prod_mode() and config.show_built_with_reflex:
             self._setup_sticky_badge()
             self._setup_sticky_badge()
 
 
-        progress.advance(task)
+        progress.update(1)
 
 
         # Store the compile results.
         # Store the compile results.
         compile_results: list[tuple[str, str]] = []
         compile_results: list[tuple[str, str]] = []
 
 
-        progress.advance(task)
+        progress.update(1)
 
 
         # Track imports and custom components found.
         # Track imports and custom components found.
         all_imports = {}
         all_imports = {}
@@ -1239,7 +1246,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 stateful_components_code,
                 stateful_components_code,
                 page_components,
                 page_components,
             ) = compiler.compile_stateful_components(self._pages.values())
             ) = 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.
         # 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:
         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))
         compile_results.append((stateful_components_path, stateful_components_code))
 
 
-        progress.advance(task)
+        progress.update(1)
 
 
         # Compile the root document before fork.
         # Compile the root document before fork.
         compile_results.append(
         compile_results.append(
@@ -1262,7 +1269,7 @@ class App(MiddlewareMixin, LifespanMixin):
             )
             )
         )
         )
 
 
-        progress.advance(task)
+        progress.update(1)
 
 
         # Copy the assets.
         # Copy the assets.
         assets_src = Path.cwd() / constants.Dirs.APP_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):
             def _submit_work(fn: Callable[..., tuple[str, str]], *args, **kwargs):
                 f = executor.submit(fn, *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)
                 result_futures.append(f)
 
 
             # Compile the pre-compiled pages.
             # Compile the pre-compiled pages.
@@ -1323,7 +1330,7 @@ class App(MiddlewareMixin, LifespanMixin):
         # Get imports from AppWrap components.
         # Get imports from AppWrap components.
         all_imports.update(app_root._get_all_imports())
         all_imports.update(app_root._get_all_imports())
 
 
-        progress.advance(task)
+        progress.update(1)
 
 
         # Compile the contexts.
         # Compile the contexts.
         compile_results.append(
         compile_results.append(
@@ -1332,13 +1339,13 @@ class App(MiddlewareMixin, LifespanMixin):
         if self.theme is not None:
         if self.theme is not None:
             # Fix #2992 by removing the top-level appearance prop
             # Fix #2992 by removing the top-level appearance prop
             self.theme.appearance = None
             self.theme.appearance = None
-        progress.advance(task)
+        progress.update(1)
 
 
         # Compile the app root.
         # Compile the app root.
         compile_results.append(
         compile_results.append(
             compiler.compile_app(app_root),
             compiler.compile_app(app_root),
         )
         )
-        progress.advance(task)
+        progress.update(1)
 
 
         # Compile custom components.
         # Compile custom components.
         (
         (
@@ -1349,8 +1356,8 @@ class App(MiddlewareMixin, LifespanMixin):
         compile_results.append((custom_components_output, custom_components_result))
         compile_results.append((custom_components_output, custom_components_result))
         all_imports.update(custom_components_imports)
         all_imports.update(custom_components_imports)
 
 
-        progress.advance(task)
-        progress.stop()
+        progress.update(1)
+        progress.finish()
 
 
         # Install frontend packages.
         # Install frontend packages.
         with console.timing("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.
     # Check the name follows the convention if picked.
     name_variants = _validate_library_name(library_name)
     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)
     _populate_custom_component_project(name_variants)
 
 
@@ -351,25 +351,25 @@ def init(
 
 
     if install:
     if install:
         package_name = name_variants.package_name
         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"]):
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
             console.info(f"Package {package_name} installed!")
         else:
         else:
             raise typer.Exit(code=1)
             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(
     console.print(
-        f"[ {CustomComponents.PACKAGE_README} ]: Package description. Please add usage examples."
+        f"[{CustomComponents.PACKAGE_README}]: Package description. Please add usage examples."
     )
     )
     console.print(
     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(
     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(
     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.
     # Validate the app name.
     app_name = prerequisites.validate_app_name(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.
     # Check prerequisites.
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
     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.
     # Reload the config to make sure the env vars are persistent.
     get_config(reload=True)
     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)
     prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
 
 

+ 2 - 14
reflex/utils/build.py

@@ -8,8 +8,6 @@ import subprocess
 import zipfile
 import zipfile
 from pathlib import Path
 from pathlib import Path
 
 
-from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
-
 from reflex import constants
 from reflex import constants
 from reflex.config import get_config
 from reflex.config import get_config
 from reflex.utils import console, path_ops, prerequisites, processes
 from reflex.utils import console, path_ops, prerequisites, processes
@@ -114,19 +112,9 @@ def _zip(
             ]
             ]
 
 
     # Create a progress bar for zipping the component.
     # 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:
         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))
             zipf.write(file, Path(file).relative_to(root_dir))
 
 
 
 

+ 211 - 30
reflex/utils/console.py

@@ -3,19 +3,211 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import contextlib
 import contextlib
+import dataclasses
 import inspect
 import inspect
 import os
 import os
+import re
 import shutil
 import shutil
+import sys
 import time
 import time
+import types
+from dataclasses import dataclass
 from pathlib import Path
 from pathlib import Path
 from types import FrameType
 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
 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 for pretty printing.
 _console = Console()
 _console = Console()
 
 
@@ -101,16 +293,13 @@ def debug(msg: str, dedupe: bool = False, **kwargs):
         kwargs: Keyword arguments to pass to the print function.
         kwargs: Keyword arguments to pass to the print function.
     """
     """
     if is_debug():
     if is_debug():
-        msg_ = f"[purple]Debug: {msg}[/purple]"
         if dedupe:
         if dedupe:
-            if msg_ in _EMITTED_DEBUG:
+            if msg in _EMITTED_DEBUG:
                 return
                 return
             else:
             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):
 def info(msg: str, dedupe: bool = False, **kwargs):
@@ -127,7 +316,8 @@ def info(msg: str, dedupe: bool = False, **kwargs):
                 return
                 return
             else:
             else:
                 _EMITTED_INFO.add(msg)
                 _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):
 def success(msg: str, dedupe: bool = False, **kwargs):
@@ -144,7 +334,8 @@ def success(msg: str, dedupe: bool = False, **kwargs):
                 return
                 return
             else:
             else:
                 _EMITTED_SUCCESS.add(msg)
                 _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):
 def log(msg: str, dedupe: bool = False, **kwargs):
@@ -161,7 +352,7 @@ def log(msg: str, dedupe: bool = False, **kwargs):
                 return
                 return
             else:
             else:
                 _EMITTED_LOGS.add(msg)
                 _EMITTED_LOGS.add(msg)
-        _console.log(msg, **kwargs)
+        _console.print(msg, **kwargs)
 
 
 
 
 def rule(title: str, **kwargs):
 def rule(title: str, **kwargs):
@@ -188,7 +379,8 @@ def warn(msg: str, dedupe: bool = False, **kwargs):
                 return
                 return
             else:
             else:
                 _EMIITED_WARNINGS.add(msg)
                 _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:
 def _get_first_non_framework_frame() -> FrameType | None:
@@ -254,7 +446,8 @@ def deprecate(
             f"removed in {removal_version}. ({loc})"
             f"removed in {removal_version}. ({loc})"
         )
         )
         if _LOG_LEVEL <= LogLevel.WARNING:
         if _LOG_LEVEL <= LogLevel.WARNING:
-            print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
+            kwargs.setdefault("color", "yellow")
+            print(f"DeprecationWarning: {msg}", **kwargs)
         if dedupe:
         if dedupe:
             _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key)
             _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key)
 
 
@@ -273,7 +466,8 @@ def error(msg: str, dedupe: bool = False, **kwargs):
                 return
                 return
             else:
             else:
                 _EMITTED_ERRORS.add(msg)
                 _EMITTED_ERRORS.add(msg)
-        print(f"[red]{msg}[/red]", **kwargs)
+        kwargs.setdefault("color", "red")
+        print(f"{msg}", **kwargs)
 
 
 
 
 def ask(
 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):
 def status(*args, **kwargs):
     """Create a status with a spinner.
     """Create a status with a spinner.
 
 
@@ -339,4 +520,4 @@ def timing(msg: str):
     try:
     try:
         yield
         yield
     finally:
     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)
     prerequisites.validate_frontend_dependencies(init=False)
 
 
     # Run the frontend in development mode.
     # 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)
     os.environ["PORT"] = str(get_config().frontend_port if port is None else port)
     run_process_and_launch_url(
     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
     # validate dependencies before run
     prerequisites.validate_frontend_dependencies(init=False)
     prerequisites.validate_frontend_dependencies(init=False)
     # Run the frontend in production mode.
     # 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(
     run_process_and_launch_url(
         [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
         [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
         backend_present,
         backend_present,

+ 1 - 1
reflex/utils/export.py

@@ -51,7 +51,7 @@ def export(
     exec.output_system_info()
     exec.output_system_info()
 
 
     # Compile the app in production mode and export it.
     # 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:
     if frontend:
         # Ensure module can be imported and app.compile() is called.
         # 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 psutil
 import typer
 import typer
 from redis.exceptions import RedisError
 from redis.exceptions import RedisError
-from rich.progress import Progress
 
 
 from reflex import constants
 from reflex import constants
 from reflex.config import environment
 from reflex.config import environment
 from reflex.utils import console, path_ops, prerequisites
 from reflex.utils import console, path_ops, prerequisites
+from reflex.utils.printer import (
+    CounterComponent,
+    FunGuyProgressComponent,
+    MessageComponent,
+    ProgressBar,
+)
 
 
 
 
 def kill(pid: int):
 def kill(pid: int):
@@ -273,7 +278,6 @@ def run_concurrently(*fns: Callable | Tuple) -> None:
 def stream_logs(
 def stream_logs(
     message: str,
     message: str,
     process: subprocess.Popen,
     process: subprocess.Popen,
-    progress: Progress | None = None,
     suppress_errors: bool = False,
     suppress_errors: bool = False,
     analytics_enabled: bool = False,
     analytics_enabled: bool = False,
 ):
 ):
@@ -282,7 +286,6 @@ def stream_logs(
     Args:
     Args:
         message: The message to display.
         message: The message to display.
         process: The process.
         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).
         suppress_errors: If True, do not exit if errors are encountered (for fallback).
         analytics_enabled: Whether analytics are enabled for this command.
         analytics_enabled: Whether analytics are enabled for this command.
 
 
@@ -297,11 +300,11 @@ def stream_logs(
     # Store the tail of the logs.
     # Store the tail of the logs.
     logs = collections.deque(maxlen=512)
     logs = collections.deque(maxlen=512)
     with process:
     with process:
-        console.debug(message, progress=progress)
+        console.debug(message)
         if process.stdout is None:
         if process.stdout is None:
             return
             return
         for line in process.stdout:
         for line in process.stdout:
-            console.debug(line, end="", progress=progress)
+            console.debug(line, end="")
             logs.append(line)
             logs.append(line)
             yield 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.
         checkpoints: The checkpoints to advance the progress bar.
     """
     """
     # Iterate over the process output.
     # 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():
 def atexit_handler():

+ 12 - 3
uv.lock

@@ -640,7 +640,7 @@ name = "importlib-metadata"
 version = "8.6.1"
 version = "8.6.1"
 source = { registry = "https://pypi.org/simple" }
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
 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 }
 sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
 wheels = [
 wheels = [
@@ -1731,10 +1731,10 @@ dependencies = [
     { name = "python-socketio" },
     { name = "python-socketio" },
     { name = "redis" },
     { name = "redis" },
     { name = "reflex-hosting-cli" },
     { name = "reflex-hosting-cli" },
-    { name = "rich" },
     { name = "setuptools" },
     { name = "setuptools" },
     { name = "sqlmodel" },
     { name = "sqlmodel" },
     { name = "starlette-admin" },
     { name = "starlette-admin" },
+    { name = "termcolor" },
     { name = "tomlkit" },
     { name = "tomlkit" },
     { name = "twine" },
     { name = "twine" },
     { name = "typer" },
     { name = "typer" },
@@ -1794,10 +1794,10 @@ requires-dist = [
     { name = "python-socketio", specifier = ">=5.7.0,<6.0" },
     { name = "python-socketio", specifier = ">=5.7.0,<6.0" },
     { name = "redis", specifier = ">=4.3.5,<6.0" },
     { name = "redis", specifier = ">=4.3.5,<6.0" },
     { name = "reflex-hosting-cli", specifier = ">=0.1.29" },
     { name = "reflex-hosting-cli", specifier = ">=0.1.29" },
-    { name = "rich", specifier = ">=13.0.0,<14.0" },
     { name = "setuptools", specifier = ">=75.0" },
     { name = "setuptools", specifier = ">=75.0" },
     { name = "sqlmodel", specifier = ">=0.0.14,<0.1" },
     { name = "sqlmodel", specifier = ">=0.0.14,<0.1" },
     { name = "starlette-admin", specifier = ">=0.11.0,<1.0" },
     { 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 = "tomlkit", specifier = ">=0.12.4,<1.0" },
     { name = "twine", specifier = ">=4.0.0,<7.0" },
     { name = "twine", specifier = ">=4.0.0,<7.0" },
     { name = "typer", specifier = ">=0.15.1,<1.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 },
     { 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]]
 [[package]]
 name = "text-unidecode"
 name = "text-unidecode"
 version = "1.3"
 version = "1.3"