Source code for framed_text.spinner

from multiprocessing import Process, Event
from sys import stdout
from time import sleep
from typing import Literal

from framed_text.labeled_data import AnyLabeledData
from framed_text.shorten_text import ShortenText
from framed_text.status import AnyStatus
from framed_text.utils import (
    ANSIEscape,
    _validate_color,
    _format_col_text,
    _get_terminal_width,
    _get_terminal_height,
)


[docs] class Spinner: BUILTIN_ICONS: dict[str, list[str]] = { "default": ['/', '-', '\\', '|'], "cube": ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], "dots": [' ', '. ', '.. ', '...'], "dots2": [' ', '. ', '.. ', '...', ' ..', ' .'], }
[docs] def __init__(self, text: str | AnyStatus | AnyLabeledData = '', icons_id: str = "default", done_icon: str = '', custom_icons: list[str] | None = None, interval: float = 0.2, position: Literal["start", "end"] = "start", indent: int = 1, spinner_color: str | tuple[int, int, int] | None = "yellow", done_color: str | tuple[int, int, int] | None = "green"): """ Draws a spinner on a screen **Position** - ``"start"``: The spinner will be placed at the start of the line. - ``"end"``: The spinner will be placed at the end of the line. :param text: Text to display next to the spinner. Omit for no text. :param icons_id: ID string of one of the built-in icon sets from ``Spinner.BUILTIN_ICONS`` :param done_icon: Icon to use for when the spinner is complete :param custom_icons: A list of strings to use as the spinner icon. Overrides ``icons_id`` :param interval: The delay between spinner icon updates. :param position: Where the spinner should be placed on a line. :param indent: How many spaces the line should be indented. :param spinner_color: Color of the spinner icon while active. :param done_color: Color for the spinner icon when stopped. """ # Terminal dimensions self._term_lines: int = 0 self._term_columns: int = 0 self._term_columns: int = _get_terminal_width() self._term_lines: int = _get_terminal_height() # Create an event that will be used to stop the spinner self._stop_e = Event() # Threading self._is_running: bool = False self._process = None # Defaults self._icons_id: str = icons_id self._interval: float = interval self._position: Literal["start", "end"] = position self._indent: int = indent self._text: str | AnyStatus | AnyLabeledData = text self._done_icon: str = done_icon self._icons: list[str] = [] self._done_color: str | tuple[int, int, int] | None = None self._spinner_color: str | tuple[int, int, int] | None = None # Setup values self.update(text=text, icons_id=icons_id, done_icon=done_icon, custom_icons=custom_icons, interval=interval, position=position, indent=indent, spinner_color=spinner_color, done_color=done_color)
[docs] def start(self, text: str = ''): """ Start the spinner :param text: Text to display next to the spinner. Omit for no text. """ if self._is_running: return if self._stop_e.is_set(): self._stop_e.clear() if text: self._text = ShortenText(text=text).__str__() self._is_running = True # Hide cursor stdout.write(ANSIEscape.HIDE_CURSOR) stdout.flush() # Start thread self._process = Process(target=self._run) self._process.start()
[docs] def stop(self, done_icon: str | None = None, done_color: str | tuple[int, int, int] | None = None): """ Stop the spinner Optionally, allow custom done icon and color for the current spinner. :param done_icon: Icon to use for when the spinner is complete :param done_color: Color for the done icon """ if self._process is None: return self._is_running = False self._stop_e.set() # Stop process self._process.join() # Cleanup self._cleanup(done_icon=done_icon, done_color=done_color) # Show cursor stdout.write(ANSIEscape.SHOW_CURSOR) stdout.flush() # Ensure enough room print()
[docs] def update(self, text: str | AnyStatus | AnyLabeledData | None = None, icons_id: str | None = None, done_icon: str | None = None, custom_icons: list[str] | None = None, interval: float | None = None, position: Literal["start", "end"] | None = None, indent: int | None = None, spinner_color: str | tuple[int, int, int] | None = None, done_color: str | tuple[int, int, int] | None = None): """ Update values for the spinner. Some values can only be updated when the spinner is not running. **Position** - ``"start"``: The spinner will be placed at the start of the line. - ``"end"``: The spinner will be placed at the end of the line. :param text: Text to display next to the spinner. Omit for no text. :param icons_id: ID string of one of the built-in icon sets from ``Spinner.BUILTIN_ICONS`` :param done_icon: Icon to use for when the spinner is complete :param custom_icons: A list of strings to use as the spinner icon. Overrides ``icons_id`` :param interval: The delay between spinner icon updates. :param position: Where the spinner should be placed on a line. :param indent: How many spaces the line should be indented. :param spinner_color: Color of the spinner icon while active. :param done_color: Color for the spinner icon when stopped. """ if not self._is_running: # Values not allowed to be changed while running self._icons_id = icons_id if icons_id else self._icons_id self._interval = interval if interval else self._interval self._position = position if position else self._position self._indent = indent if indent else self._indent if spinner_color: _validate_color(color=spinner_color) self._spinner_color = spinner_color.lower() if isinstance(spinner_color, str) else spinner_color # Setup icons if custom_icons: # Custom icons self._icons = [icon[0] for icon in custom_icons] elif icons_id: # Built-in icons if icons_id in list(self.BUILTIN_ICONS.keys()): self._icons = self.BUILTIN_ICONS[icons_id] else: raise ValueError(f"Invalid Icon ID: {icons_id}\nValid Icon IDs: {list(self.BUILTIN_ICONS.keys())}") else: # Default icons self._icons = self.BUILTIN_ICONS["default"] # Values allowed to be changed while running self._done_icon = done_icon if done_icon else self._done_icon if done_color: _validate_color(color=done_color) self._done_color = done_color.lower() if isinstance(done_color, str) else done_color # Resolve text self._text = ShortenText(text=text).__str__() if text else self._text
def _cleanup(self, done_icon: str | None = None, done_color: str | tuple[int, int, int] | None = None): """ Remove or replace the spinner based on position :param done_icon: Icon to use for when the spinner is complete :param done_color: Color for the spinner icon when stopped. """ icon: str = '' stdout.write(ANSIEscape.ERASE_LINE) _done_color = done_color if done_color else self._done_color _done_icon = done_icon if done_icon else self._done_icon if self._position == "start": if _done_icon: icon = _format_col_text(text=f'{_done_icon[0]} ', color=_done_color) stdout.write(f"\r{' ' * self._indent}{icon}{self._text}") elif self._position == "end": if _done_icon: icon = _format_col_text(text=f' {_done_icon[0]}', color=_done_color) stdout.write(f"\r{' ' * self._indent}{self._text}{icon}") def _run(self): """ Function for ``self._process`` to run and display the spinner """ try: itr: int = 0 cur_icon: str = _format_col_text(text=self._icons[itr], color=self._spinner_color) while not self._stop_e.is_set(): # Print spinner if self._position == "start": stdout.write(f"\r{' ' * self._indent}{cur_icon} {self._text}") elif self._position == "end": stdout.write(f"\r{' ' * self._indent}{self._text} {cur_icon}") # Move to next icon itr = (itr + 1) % len(self._icons) cur_icon: str = _format_col_text(text=self._icons[itr], color=self._spinner_color) sleep(self._interval) except (EOFError, KeyboardInterrupt): # Clean up and show cursor self._cleanup(done_icon='✘', done_color="red") stdout.write(ANSIEscape.SHOW_CURSOR) stdout.flush()