From 3ab468205f9eea484b0e71cdb8f8ec302596e7a0 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 10 Nov 2025 16:29:31 +0100 Subject: [PATCH] add CLI loading animation, closes #4 --- pyproject.toml | 4 +-- src/dopt_basics/cli.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/dopt_basics/cli.py diff --git a/pyproject.toml b/pyproject.toml index 7105984..fc000ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dopt-basics" -version = "0.2.0" +version = "0.2.1" description = "basic cross-project tools for Python-based d-opt projects" authors = [ {name = "Florian Förster", email = "f.foerster@d-opt.com"}, @@ -69,7 +69,7 @@ directory = "reports/coverage" [tool.bumpversion] -current_version = "0.2.0" +current_version = "0.2.1" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. diff --git a/src/dopt_basics/cli.py b/src/dopt_basics/cli.py new file mode 100644 index 0000000..94c52b9 --- /dev/null +++ b/src/dopt_basics/cli.py @@ -0,0 +1,68 @@ +import shutil +import time +import typing as t +from collections.abc import Callable +from functools import wraps +from itertools import cycle +from threading import Thread + +P = t.ParamSpec("P") +T = t.TypeVar("T") + + +# based on: https://stackoverflow.com/questions/22029562/python-how-to-make-simple-animated-loading-while-process-is-running +class LoadingAnimation: + def __init__( + self, + loading_txt: str, + ending_text: str, + timeout: float = 0.1, + ) -> None: + self.loading_txt = loading_txt + self.ending_text = ending_text + self.timeout = timeout + self.done: bool = False + self.steps: list[str] = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"] + + self._thread: Thread = Thread(target=self._animation, daemon=True) + + def __enter__(self) -> None: + self.start() + + def __exit__( + self, + exc_type, + exc_value, + tb, + ) -> None: + self.stop() + + def _animation(self) -> None: + for c in cycle(self.steps): + if self.done: + break + print(f"\r{c} {self.loading_txt}", flush=True, end="") + time.sleep(self.timeout) + + def start(self) -> None: + self._thread.start() + + def stop(self): + self.done = True + cols = shutil.get_terminal_size((80, 20)).columns + print("\r" + " " * cols, end="", flush=True) + print(f"\r{self.ending_text}", flush=True) + + +def default_loading(func: Callable[P, T]) -> Callable[P, T]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + loading_txt: str = "Performing operation..." + ending_text: str = "Operation successful" + + with LoadingAnimation(loading_txt, ending_text): + res = func(*args, **kwargs) + + return res + + return wrapper