hubble-cli 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hubble/spinner.py ADDED
@@ -0,0 +1,77 @@
1
+ """Animated activity indicator: pulsing glyph, rotating verbs and a shimmer sweeping over the text.
2
+
3
+ Used through rich.live.Live, which calls __rich__ on every refresh, so the animation is purely
4
+ time-based and costs nothing between frames.
5
+ """
6
+
7
+ import random
8
+ import time
9
+ from typing import Optional
10
+
11
+ from rich.live import Live
12
+ from rich.text import Text
13
+
14
+ VERBS = [
15
+ "Thinking", "Stargazing", "Orbiting", "Focusing the lens", "Aligning mirrors", "Charting the stars",
16
+ "Scanning the cosmos", "Decoding signals", "Calculating trajectories", "Mapping nebulae", "Pondering",
17
+ "Drifting through code", "Collecting photons", "Warping", "Consulting the stars", "Tuning the antenna",
18
+ "Surveying galaxies", "Cogitating", "Tinkering", "Noodling", "Contemplating", "Launching probes",
19
+ ]
20
+ GLYPHS = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"]
21
+
22
+ # Warm shimmer palette: base, near highlight, highlight.
23
+ BASE, NEAR, PEAK = "#d7875f", "#ffaf87", "bold #ffe7d7"
24
+ GLYPH_STYLE = "bold #ff875f"
25
+
26
+
27
+ class Shimmer:
28
+ def __init__(self, label: Optional[str] = None, hint: str = "ctrl+c to interrupt", details: bool = True):
29
+ self.start = time.time()
30
+ self.details = details
31
+ self.label = label
32
+ self.hint = hint
33
+ self.tokens = 0
34
+ self._offset = random.randrange(len(VERBS))
35
+
36
+ def set_label(self, label: Optional[str]):
37
+ self.label = label
38
+
39
+ def __rich__(self) -> Text:
40
+ t = time.time() - self.start
41
+ glyph = GLYPHS[int(t * 10) % len(GLYPHS)]
42
+ words = (self.label or VERBS[(int(t / 3.5) + self._offset) % len(VERBS)]) + "…"
43
+ sweep = len(words) + 10
44
+ pos = (t * 16) % sweep - 5 # highlight position; runs off both ends for a pause between sweeps
45
+ out = Text(f"{glyph} ", style=GLYPH_STYLE)
46
+ for i, ch in enumerate(words):
47
+ d = abs(i - pos)
48
+ out.append(ch, style=PEAK if d < 1.2 else NEAR if d < 3 else BASE)
49
+ if not self.details:
50
+ return out
51
+ details = [f"{int(t)}s"]
52
+ if self.tokens:
53
+ details.append(f"↓ {self.tokens:,} tokens")
54
+ if self.hint:
55
+ details.append(self.hint)
56
+ out.append(f" ({' · '.join(details)})", style="dim")
57
+ return out
58
+
59
+
60
+ def start_shimmer(console, label: Optional[str] = None) -> "ShimmerLive":
61
+ live = ShimmerLive(Shimmer(label), console)
62
+ live.start()
63
+ return live
64
+
65
+
66
+ class ShimmerLive:
67
+ """Live display with the same start/stop interface as rich's Status."""
68
+
69
+ def __init__(self, shimmer: Shimmer, console):
70
+ self.shimmer = shimmer
71
+ self.live = Live(shimmer, console=console, refresh_per_second=15, transient=True)
72
+
73
+ def start(self):
74
+ self.live.start()
75
+
76
+ def stop(self):
77
+ self.live.stop()