-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspinner.py
More file actions
163 lines (124 loc) · 4.31 KB
/
spinner.py
File metadata and controls
163 lines (124 loc) · 4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Spinner for showing loading state."""
from __future__ import annotations
import sys
import threading
import time
from typing import TextIO
from rich.console import Console
from .symbols import S_BAR, S_ERROR, S_STEP_CANCEL, S_STEP_SUBMIT, S_WARN, UNICODE
class Spinner:
"""An animated spinner for showing loading state.
Example:
>>> s = spinner()
>>> s.start("Loading...")
>>> # do work
>>> s.stop("Done!")
"""
# Spinner frames
FRAMES = ["◒", "◐", "◓", "◑"]
FRAMES_ASCII = ["|", "/", "-", "\\"]
def __init__(self, output: TextIO | None = None) -> None:
"""Initialize the spinner.
Args:
output: Output stream (defaults to stdout).
"""
self._output = output or sys.stdout
self._console = Console(file=self._output, highlight=False)
self._message = ""
self._running = False
self._thread: threading.Thread | None = None
self._frame_index = 0
# Use ASCII frames if Unicode not supported
self._frames = self.FRAMES if UNICODE else self.FRAMES_ASCII
def _animate(self) -> None:
"""Animation loop running in background thread."""
while self._running:
frame = self._frames[self._frame_index % len(self._frames)]
self._frame_index += 1
# Write directly to output (no Rich markup in animation)
# Use carriage return to overwrite the line
line = f"\r{S_BAR} {frame} {self._message}"
self._output.write(line)
self._output.flush()
time.sleep(0.08)
def start(self, message: str = "") -> Spinner:
"""Start the spinner.
Args:
message: Message to display next to spinner.
Returns:
Self for chaining.
"""
if self._running:
return self
self._message = message
self._running = True
self._frame_index = 0
# Start animation thread
self._thread = threading.Thread(target=self._animate, daemon=True)
self._thread.start()
return self
def _stop_animation(self) -> None:
"""Stop the animation thread."""
self._running = False
if self._thread:
self._thread.join(timeout=0.5)
self._thread = None
def stop(self, message: str = "", code: int = 0) -> None:
"""Stop the spinner with a final message.
Args:
message: Final message to display.
code: Exit code (0=success, 1=error, 2=warning, 3=cancel).
"""
self._stop_animation()
# Clear line with carriage return and spaces
self._output.write("\r" + " " * 80 + "\r")
self._output.flush()
# Choose symbol based on code
if code == 0:
symbol = f"[green]{S_STEP_SUBMIT}[/green]"
elif code == 1:
symbol = f"[red]{S_ERROR}[/red]"
elif code == 2:
symbol = f"[yellow]{S_WARN}[/yellow]"
else:
symbol = f"[red]{S_STEP_CANCEL}[/red]"
msg = message or self._message
self._console.print(f"[bright_black]{S_BAR}[/bright_black]")
self._console.print(f"{symbol} {msg}")
def error(self, message: str = "") -> None:
"""Stop with an error.
Args:
message: Error message to display.
"""
self.stop(message, code=1)
def warn(self, message: str = "") -> None:
"""Stop with a warning.
Args:
message: Warning message to display.
"""
self.stop(message, code=2)
def cancel(self, message: str = "") -> None:
"""Stop with cancellation.
Args:
message: Cancellation message to display.
"""
self.stop(message, code=3)
def message(self, message: str) -> None:
"""Update the spinner message.
Args:
message: New message to display.
"""
self._message = message
def spinner(output: TextIO | None = None) -> Spinner:
"""Create a new spinner instance.
Args:
output: Output stream (defaults to stdout).
Returns:
A new Spinner instance.
Example:
>>> s = spinner()
>>> s.start("Loading data...")
>>> # do work
>>> s.stop("Data loaded!")
"""
return Spinner(output)