fix(tui): restore LoadingThrobber widget and tui_throbber robot tests
The LoadingThrobber widget and its associated Robot Framework integration tests were developed on the feat/issue-6357-tui-loading-states branch but were never merged to master. This commit restores: - robot/tui_throbber.robot: Integration tests for the LoadingThrobber widget covering show/hide cycle, style switching, and invalid style rejection - src/cleveragents/tui/widgets/throbber.py: The LoadingThrobber implementation with rainbow and quotes animation styles - src/cleveragents/tui/quotes.py: Quote loader module for the throbber - src/cleveragents/tui/data/throbber_quotes.txt: Curated quote collection The failing test 'Throbber Rejects Invalid Styles' in robot/tui_throbber.robot was identified by UAT testing [AUTO-UAT-3] as failing because the production code (LoadingThrobber) was missing from master while the test file was expected to be present. ISSUES CLOSED: #6357
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
*** Settings ***
|
||||
Documentation Integration coverage for the CleverAgents TUI loading throbber.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Library Process
|
||||
Library Collections
|
||||
Suite Setup Setup Test Environment With Database Isolation
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
|
||||
*** Test Cases ***
|
||||
Throbber Show Hide Cycle Updates Visibility
|
||||
[Documentation] The throbber toggles visibility attributes when shown and hidden.
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... from types import SimpleNamespace
|
||||
... from cleveragents.tui.widgets.throbber import LoadingThrobber
|
||||
... throbber = LoadingThrobber(style="rainbow")
|
||||
... throbber.styles = SimpleNamespace(height=None, min_height=None, padding=None)
|
||||
... assert throbber.is_running is False, "Expected throbber idle on init"
|
||||
... throbber.show_loading()
|
||||
... assert throbber.is_running is True, "Expected throbber running after show_loading()"
|
||||
... assert getattr(throbber, "display", True) is True, "display should be True after show_loading()"
|
||||
... assert getattr(throbber.styles, "height", None) == 1, "height should be 1 when visible"
|
||||
... assert getattr(throbber.styles, "min_height", None) == 1, "min_height should be 1 when visible"
|
||||
... throbber.hide_loading()
|
||||
... assert throbber.is_running is False, "Expected throbber idle after hide_loading()"
|
||||
... assert getattr(throbber, "display", False) is False, "display should be False after hide_loading()"
|
||||
... assert getattr(throbber.styles, "height", None) == 0, "height should collapse when hidden"
|
||||
... assert getattr(throbber.styles, "min_height", None) == 0, "min_height should collapse when hidden"
|
||||
... print("throbber-visibility-ok")
|
||||
${result}= Run Process ${PYTHON} -c ${script} shell=False timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} throbber-visibility-ok
|
||||
|
||||
Throbber Style Switch Preserves Running State
|
||||
[Documentation] Switching styles while running keeps animation active and rotates quotes.
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... from types import SimpleNamespace
|
||||
... from cleveragents.tui.widgets.throbber import LoadingThrobber
|
||||
... throbber = LoadingThrobber(style="rainbow", quotes=("alpha", "beta", "gamma", "delta"))
|
||||
... throbber.styles = SimpleNamespace(height=None, min_height=None, padding=None)
|
||||
... throbber.show_loading()
|
||||
... assert throbber.is_running is True, "Expected throbber to run after show_loading()"
|
||||
... throbber.set_style("quotes")
|
||||
... assert throbber.is_running is True, "Expected throbber to remain running after style change"
|
||||
... quotes_before = list(throbber._quotes)
|
||||
... throbber.hide_loading()
|
||||
... throbber.show_loading()
|
||||
... quotes_after = list(throbber._quotes)
|
||||
... assert quotes_before != quotes_after, "Quotes deque should rotate between animations"
|
||||
... throbber.hide_loading()
|
||||
... print("throbber-style-ok")
|
||||
${result}= Run Process ${PYTHON} -c ${script} shell=False timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} throbber-style-ok
|
||||
|
||||
Throbber Rejects Invalid Styles
|
||||
[Documentation] Invalid styles raise ValueError to protect configuration flow.
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... from types import SimpleNamespace
|
||||
... from cleveragents.tui.widgets.throbber import LoadingThrobber
|
||||
... throbber = LoadingThrobber()
|
||||
... throbber.styles = SimpleNamespace(height=None, min_height=None, padding=None)
|
||||
... try:
|
||||
... throbber.set_style("blink")
|
||||
... except ValueError:
|
||||
... print("throbber-invalid-style-ok")
|
||||
... else:
|
||||
... raise AssertionError("Expected ValueError for invalid throbber style")
|
||||
${result}= Run Process ${PYTHON} -c ${script} shell=False timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} throbber-invalid-style-ok
|
||||
@@ -0,0 +1,210 @@
|
||||
"I'm sorry, Dave. I'm afraid I can't do that." — HAL 9000
|
||||
"Any sufficiently advanced technology is indistinguishable from magic." — Arthur C. Clarke
|
||||
"Logic is the beginning of wisdom, not the end." — Spock
|
||||
"The future is already here — it's just not evenly distributed." — William Gibson
|
||||
"The robots of the world pointed out that they fall no more than humans do." — Isaac Asimov
|
||||
"We've all got both light and dark inside us. What matters is the part we choose to act on." — Sirius Black
|
||||
"Reality is that which, when you stop believing in it, doesn't go away." — Philip K. Dick
|
||||
"Sometimes the questions are complicated and the answers are simple." — Dr. Seuss
|
||||
"Never tell me the odds!" — Han Solo
|
||||
"Do. Or do not. There is no try." — Yoda
|
||||
"Dreams that become reality are simply plans with courage stitched in." — Nova Armitage
|
||||
"You want weapons? We're in a library! Books!" — The Doctor
|
||||
"So shines a good deed in a weary world." — Willy Wonka
|
||||
"The sky above the port was the color of television, tuned to a dead channel." — William Gibson
|
||||
"Against the lurid glow of gamma storms we still debug our future." — Selene Tarkasian
|
||||
"The answer's out there, Neo. It's looking for you, and it will find you if you want it to." — Trinity
|
||||
"In the long run, the sharpest weapon of all is a kind and gentle spirit." — Anne Frank
|
||||
"A small leak will sink a great ship, but a small patch can sail the stars." — Mara Dyson
|
||||
"He who controls the spice controls the universe." — Frank Herbert
|
||||
"Hope is the first step on the road to disappointment." — Commissar Ciaphas Cain
|
||||
"Yet, in that dim corner of the lab, the prototype winked with possibility." — Eliana Trent
|
||||
"To boldly go where no one has gone before." — Jean-Luc Picard
|
||||
"Open the pod bay doors, HAL." — Dave Bowman
|
||||
"You can't stop the signal." — Mr. Universe
|
||||
"We're all stories in the end. Make it a good one." — The Doctor
|
||||
"Somewhere, something incredible is waiting to be known." — Carl Sagan
|
||||
"An elegant weapon for a more civilized age." — Obi-Wan Kenobi
|
||||
"Fear is the mind-killer." — Frank Herbert
|
||||
"The needs of the many outweigh the needs of the few, or the one." — Spock
|
||||
"I have been, and always shall be, your friend." — Spock
|
||||
"The greatest minds are fueled by curiosity cloaked in starlight." — Juniper Kade
|
||||
"The universe is a pretty big place. If it's just us, seems like an awful waste of space." — Carl Sagan
|
||||
"Light thinks it travels faster than anything but it is wrong." — Terry Pratchett
|
||||
"We can rebuild him. We have the technology." — Oscar Goldman
|
||||
"Even the smallest byte can corrupt the empire of silence." — Hester Shaw
|
||||
"Fortune favors the bold, especially the lightly caffeinated coder." — Mira Sun
|
||||
"It's dangerous to go alone! Take this." — The Old Man
|
||||
"Science fiction is the faultline where imagination meets inevitability." — C. J. Cherryh
|
||||
"Breathe. Just breathe." — Leia Organa
|
||||
"The best way to predict the future is to invent it." — Alan Kay
|
||||
"Don't panic." — Douglas Adams
|
||||
"Who controls the past controls the future." — George Orwell
|
||||
"At the speed of thought we still stop for a good log message." — Kiona Black
|
||||
"It is the mark of an educated mind to be able to entertain a thought without accepting it." — Aristotle
|
||||
"If you want to build a better world, start by naming your variables kindly." — Arjun Tse
|
||||
"The stars are not even remote as they are inconsistent." — H. G. Wells
|
||||
"Time is an illusion. Lunchtime doubly so." — Douglas Adams
|
||||
"First we guess. Then we calculate without mercy." — Dr. Ray Kurin
|
||||
"Humanity didn't ask the machines for permission to dream." — Tessa Quinlan
|
||||
"Stay a while and listen." — Deckard Cain
|
||||
"With great power comes great responsibility." — Uncle Ben
|
||||
"The most merciful thing in the world is the inability of the human mind to correlate all its contents." — H. P. Lovecraft
|
||||
"When in doubt, take a deep breath and read the spec again." — Priya Solace
|
||||
"I've seen things you people wouldn't believe." — Roy Batty
|
||||
"What do we say to the God of Death? Not today." — Syrio Forel
|
||||
"Keep your secrets in version control, not in your heart." — Dax Morrow
|
||||
"Life finds a way." — Ian Malcolm
|
||||
"Of all the souls I have encountered in my travels, his was the most human." — James T. Kirk
|
||||
"The night is darkest just before the dawn." — Harvey Dent
|
||||
"A computer once beat me at chess, but it was no match for me at kickboxing." — Emo Philips
|
||||
"Every program has two purposes: the one for which it was written and another for which it wasn't." — Peter Neumann
|
||||
"PATTERN RECOGNIZED. Initiate kindness subroutine." — Service Drone 7
|
||||
"Captain, I believe I speak for everyone here when I say... huh?" — Data
|
||||
"The cosmos is within us. We are made of star-stuff." — Carl Sagan
|
||||
"The limits of my language mean the limits of my world." — Ludwig Wittgenstein
|
||||
"Evolution has made it this far on chance and determination. Software needs tests." — Eileen Paik
|
||||
"We need to fail down here so we don't fail up there." — NASA Engineer in Apollo 13
|
||||
"The future depends on what you do today." — Mahatma Gandhi
|
||||
"Wisdom begins in wonder, but deliverables end in checklists." — Orion Valdez
|
||||
"Your scientists were so preoccupied with whether they could, they didn't stop to think if they should." — Ian Malcolm
|
||||
"There is no spoon." — Spoon Boy
|
||||
"All your base are belong to us." — CATS
|
||||
"Before software can be reusable it first has to be usable." — Ralph Johnson
|
||||
"The simplest things are often the truest." — Richard Bach
|
||||
"In the long shadow of the nebula, we still pair program." — Lyra Calder
|
||||
"Wake up, Neo... the matrix has you." — Morpheus
|
||||
"No fate but what we make for ourselves." — Sarah Connor
|
||||
"If you want to know what a man’s like, take a good look at how he treats his inferiors." — Sirius Black
|
||||
"Cat videos are the only known universal constant of the multiverse." — Quinn Zhao
|
||||
"The future is bright enough to require calibrated exposure." — Anika Ren
|
||||
"You're gonna need a bigger boat." — Martin Brody
|
||||
"Sometimes we have to look to the past to move forward." — Shuri
|
||||
"We are the music makers, and we are the dreamers of dreams." — Arthur O'Shaughnessy
|
||||
"It's not enough to bash in heads, you've got to bash in minds." — Joss Whedon
|
||||
"Machine dreams are written in firmware but interpreted in hope." — Aziza Hale
|
||||
"The engine of progress hums on feedback loops and fearless debugging." — Kestrel DuPont
|
||||
"No amount of screaming will make the compiler kinder. Reconsider your tone." — Lark Vega
|
||||
"I find your lack of faith disturbing." — Darth Vader
|
||||
"Travel far enough, you meet yourself." — David Mitchell
|
||||
"Patience is the companion of wisdom." — Saint Augustine
|
||||
"I know kung fu." — Neo
|
||||
"Just once I'd like to land somewhere and say, 'It's smaller on the outside.'" — The Doctor
|
||||
"Faith manages." — Jeffrey Sinclair
|
||||
"Progress tastes like solder and espresso." — Nico Solari
|
||||
"The future isn't written, but we can at least comment our intentions." — Rowan Estrada
|
||||
"Dreaming permits each of us to be quietly and safely insane every night." — William C. Dement
|
||||
"If you eliminate the impossible, whatever remains, however improbable, must be the truth." — Sherlock Holmes
|
||||
"The universe is under no obligation to make sense to you." — Neil deGrasse Tyson
|
||||
"Programming isn't about what you know; it's about what you can figure out." — Chris Pine
|
||||
"We are star stuff harvesting sunlight." — Carl Sagan
|
||||
"The first principle is that you must not fool yourself." — Richard Feynman
|
||||
"Trust the process; distrust the unreviewed commit." — Indigo Reyes
|
||||
"Not all treasure is silver and gold." — Jack Sparrow
|
||||
"When the storm comes, you don't need a hero. You need a plan." — Vega Laurent
|
||||
"On my world, we have a saying: don't forget to hydrate before saving the galaxy." — Captain Rexa
|
||||
"Knowledge speaks, but wisdom listens." — Jimi Hendrix
|
||||
"Every line of code is a promise to the future." — Idris Monroe
|
||||
"In a world where you can be anything, be kind." — Jennifer Dukes Lee
|
||||
"Entropy always wins unless curiosity reroutes the outcome." — Calla Reeves
|
||||
"The greatest victory is that which requires no battle." — Sun Tzu
|
||||
"I am one with the Force and the Force is with me." — Chirrut Îmwe
|
||||
"We all have our time machines, don't we." — H. G. Wells
|
||||
"Keep watching the skies." — The Thing from Another World
|
||||
"If you're going through hell, keep going." — Winston Churchill
|
||||
"Courage is found in unlikely places." — J. R. R. Tolkien
|
||||
"Starlight is broadband for the soul." — Amina Solari
|
||||
"The world changed because we decided to route around failure." — Harper Quill
|
||||
"Your focus determines your reality." — Qui-Gon Jinn
|
||||
"This is the way." — The Armorer
|
||||
"Chance favors the prepared mind." — Louis Pasteur
|
||||
"You step onto the road, and if you don't keep your feet, there's no knowing where you might be swept off to." — Bilbo Baggins
|
||||
"Failure is simply the opportunity to begin again, this time more intelligently." — Henry Ford
|
||||
"The best dreams happen when you're awake." — Cherie Gilderbloom
|
||||
"Even the wind follows a protocol when it circles the dunes." — Cassian Holt
|
||||
"Input is life. Output is legacy." — Vana Idril
|
||||
"Leap, and the net will appear." — John Burroughs
|
||||
"The greatest risk is not taking one." — Mellody Hobson
|
||||
"Energy cannot be created or destroyed, but software can be refactored." — Pax Leland
|
||||
"Wisdom comes through suffering." — Aeschylus
|
||||
"Light the signal, synchronize the clocks, and move with intention." — Rhea Calder
|
||||
"The purpose of life is to contribute in some way to making things better." — Robert F. Kennedy
|
||||
"Destiny is just resilience wearing a cape." — Talin Frost
|
||||
"Curiosity is the wick in the candle of learning." — William Arthur Ward
|
||||
"For small creatures such as we the vastness is bearable only through love." — Carl Sagan
|
||||
"We cannot solve our problems with the same thinking we used when we created them." — Albert Einstein
|
||||
"Every galaxy started as a whisper." — Liora Kane
|
||||
"Learn the rules like a pro, so you can break them like an artist." — Pablo Picasso
|
||||
"Precision is kindness to the future maintainers." — Ezra Marquez
|
||||
"I rebel." — Jyn Erso
|
||||
"Keep holding on. The night is darkest right before the dawn." — Rachel Dawes
|
||||
"Risk is our business." — James T. Kirk
|
||||
"Do not go gentle into that good night." — Dylan Thomas
|
||||
"Imagination is the only weapon in the war against reality." — Lewis Carroll
|
||||
"Stories are data with a soul." — Brené Brown
|
||||
"We didn't come this far to only come this far." — Lexi Bishop
|
||||
"Beware the quiet ones; they're writing the tests." — Maren Leto
|
||||
"The journey of a thousand miles begins with a single step." — Lao Tzu
|
||||
"It's not science if you know what you're doing." — Sheldon Cooper
|
||||
"History is written by those with backups." — Leland Cortez
|
||||
"Great things are done by a series of small things brought together." — Vincent van Gogh
|
||||
"The programmer is the device that turns coffee into software." — Piotr J. Gack
|
||||
"An idea isn't responsible for the people who believe in it." — Don Marquis
|
||||
"Signal and noise are siblings, raised by discipline." — Rian Talbot
|
||||
"Guard the map, share the compass." — Leyla Quinn
|
||||
"In space the stars make no sound, but the logs are verbose." — Orion Pax
|
||||
"You must unlearn what you have learned." — Yoda
|
||||
"Stand in the place where you work and look to the future." — Michael Stipe
|
||||
"Let the past die. Kill it, if you have to." — Kylo Ren
|
||||
"There is no innovation and creativity without failure. Period." — Brené Brown
|
||||
"Gravity is a habit that is hard to shake off." — Terry Pratchett
|
||||
"Every null pointer is a reminder to stay humble." — Kavi Song
|
||||
"Sometimes it's the people no one imagines anything of who do the things that no one can imagine." — Alan Turing
|
||||
"For the ones who dream of stranger worlds." — V. E. Schwab
|
||||
"Stay hungry, stay foolish." — Steve Jobs
|
||||
"Your time is limited, so don't waste it living someone else's life." — Steve Jobs
|
||||
"Believe in the impossible and push the button anyway." — Lysa Adair
|
||||
"A rising tide lifts all spaceships." — Commander Talvek
|
||||
"You are the sky. Everything else — it's just the weather." — Pema Chödrön
|
||||
"Outside of a dog, a book is a man's best friend." — Groucho Marx
|
||||
"Not all those who wander are lost." — J. R. R. Tolkien
|
||||
"Wizards are just coders with better robes." — Mira Halden
|
||||
"One voice can change a room, and if it can change a room, it can change a city." — Barack Obama
|
||||
"Kind words cost nothing and accomplish everything." — Maya Angelou
|
||||
"Keep your face always toward the sunshine." — Walt Whitman
|
||||
"Whether you think you can or you think you can't, you're right." — Henry Ford
|
||||
"Every decision we make echoes in loops of causality." — Aria Kepler
|
||||
"Standby is just another word for poised." — Jace Ryland
|
||||
"We are the strange loop that dreams of itself." — Douglas Hofstadter
|
||||
"Let us step into the night and pursue that flighty temptress, adventure." — Albus Dumbledore
|
||||
"Even in code, poetry finds a place." — Hana Mirek
|
||||
"The future rewards those who press refresh and keep iterating." — Peyton Cross
|
||||
"Hard choices, easy life. Easy choices, hard life." — Jerzy Gregorek
|
||||
"A hero is someone who understands the responsibility that comes with their freedom." — Bob Dylan
|
||||
"Sufficiently documented hacks are indistinguishable from features." — Vera Lyric
|
||||
"Whatever you are, be a good one." — Abraham Lincoln
|
||||
"Consoles are just confessionals with stack traces." — Dillon Aru
|
||||
"You cannot swim for new horizons until you have courage to lose sight of the shore." — William Faulkner
|
||||
"Make it so." — Jean-Luc Picard
|
||||
"We do these things not because they are easy, but because they are hard." — John F. Kennedy
|
||||
"Even stardust needs structure." — Penelope Acharya
|
||||
"We are all apprentices in a craft where no one ever becomes a master." — Ernest Hemingway
|
||||
"Trust, but verify." — Ronald Reagan
|
||||
"No mute button for conscience exists." — Astra Rune
|
||||
"The price of greatness is responsibility." — Winston Churchill
|
||||
"You miss 100% of the shots you don't take." — Wayne Gretzky
|
||||
"The sufferings of our planet are the sufferings of our choices." — Greta Thunberg
|
||||
"Our doubts are traitors." — William Shakespeare
|
||||
"Precision beats power, and timing beats speed." — Conor McGregor
|
||||
"Be who you needed when you were younger." — Ayesha Siddiqi
|
||||
"Once you eliminate your number-one problem, you promote number two." — Gerhard Gschwandtner
|
||||
"Measure twice, deploy once." — Rafi Goldman
|
||||
"Curiosity is the supreme virtue of explorers and engineers alike." — Naledi Star
|
||||
"Woe unto the code that forgets its tests." — Gideon Locke
|
||||
"There's always room for a story that can transport people to another place." — J. K. Rowling
|
||||
"We are what we repeatedly do. Excellence, then, is not an act, but a habit." — Will Durant
|
||||
"One dream begins every time you press compile." — Mirae Lang
|
||||
"Celebrate the wins, even the tiny ones." — Zora Hale
|
||||
"When you see a fork in the road, take it." — Yogi Berra
|
||||
"Keep moving forward." — Meet the Robinsons
|
||||
"Stay curious." — CleverAgents Motto
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Curated quote collection used by the TUI throbber."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
_DATA_PATH = Path(__file__).with_name("data").joinpath("throbber_quotes.txt")
|
||||
_FALLBACK_QUOTES: tuple[str, ...] = (
|
||||
"\"I'm sorry, Dave. I'm afraid I can't do that.\" — HAL 9000",
|
||||
(
|
||||
'"Any sufficiently advanced technology is indistinguishable from magic." '
|
||||
"— Arthur C. Clarke"
|
||||
),
|
||||
'"Logic is the beginning of wisdom, not the end." — Spock',
|
||||
)
|
||||
|
||||
|
||||
def _load_quotes() -> tuple[str, ...]:
|
||||
"""Load the throbber quotes from the data file.
|
||||
|
||||
Falls back to a small default set if the data file cannot be read so the
|
||||
widget continues to operate during packaging or test environments.
|
||||
"""
|
||||
|
||||
try:
|
||||
lines = _DATA_PATH.read_text(encoding="utf-8").splitlines()
|
||||
except FileNotFoundError:
|
||||
return _FALLBACK_QUOTES
|
||||
|
||||
quotes = tuple(line.strip() for line in lines if line.strip())
|
||||
if quotes:
|
||||
return quotes
|
||||
return _FALLBACK_QUOTES
|
||||
|
||||
|
||||
THROBBER_QUOTES: tuple[str, ...] = _load_quotes()
|
||||
|
||||
|
||||
__all__ = ["THROBBER_QUOTES"]
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Loading throbber widget used to indicate background activity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import random
|
||||
from collections import deque
|
||||
from collections.abc import Sequence
|
||||
from contextlib import suppress
|
||||
from typing import Any, cast
|
||||
|
||||
from cleveragents.tui.quotes import THROBBER_QUOTES
|
||||
|
||||
|
||||
def _load_static_base() -> type[Any]:
|
||||
"""Return the Textual Static base class or a lightweight fallback."""
|
||||
|
||||
try:
|
||||
return importlib.import_module("textual.widgets").Static
|
||||
except Exception: # pragma: no cover - textual not installed
|
||||
|
||||
class _FallbackStatic:
|
||||
"""Minimal stand-in mirroring the API used by the widget."""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
self._text = ""
|
||||
self.visible = True
|
||||
|
||||
def update(self, renderable: object) -> None: # pragma: no cover - trivial
|
||||
self._text = str(renderable)
|
||||
|
||||
return _FallbackStatic
|
||||
|
||||
|
||||
try: # pragma: no branch - optional dependency
|
||||
_RICH_TEXT_CLS: type[Any] | None = importlib.import_module("rich.text").Text
|
||||
except Exception: # pragma: no cover - rich unavailable when textual missing
|
||||
_RICH_TEXT_CLS = None
|
||||
|
||||
|
||||
_StaticBase = _load_static_base()
|
||||
|
||||
|
||||
_RAINBOW_COLORS: tuple[str, ...] = (
|
||||
"#881177",
|
||||
"#aa3355",
|
||||
"#cc6666",
|
||||
"#ee9944",
|
||||
"#eedd00",
|
||||
"#99dd55",
|
||||
"#44dd88",
|
||||
"#22ccbb",
|
||||
"#00bbcc",
|
||||
"#0099cc",
|
||||
"#3366bb",
|
||||
"#663399",
|
||||
)
|
||||
|
||||
|
||||
class LoadingThrobber(_StaticBase):
|
||||
"""Animated loading indicator supporting rainbow and quote styles."""
|
||||
|
||||
_RAINBOW_FPS: float = 15.0
|
||||
_QUOTES_PERIOD_SECONDS: float = 3.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
style: str = "rainbow",
|
||||
quotes: Sequence[str] | None = None,
|
||||
id: str | None = None,
|
||||
) -> None:
|
||||
super().__init__("", id=id)
|
||||
self._style = "rainbow"
|
||||
self._rainbow_offset = 0
|
||||
self._running = False
|
||||
self._rainbow_timer: Any | None = None
|
||||
self._quotes_timer: Any | None = None
|
||||
self._quotes: deque[str] = deque(quotes or THROBBER_QUOTES)
|
||||
self._rng = random.Random()
|
||||
self.set_style(style)
|
||||
self._apply_visibility(False)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle hooks
|
||||
# ------------------------------------------------------------------
|
||||
def on_mount(self) -> None: # pragma: no cover - relies on Textual runtime
|
||||
self._apply_visibility(False)
|
||||
if self._style == "quotes":
|
||||
self._shuffle_quotes()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
def set_style(self, style: str) -> None:
|
||||
"""Change the throbber style ("rainbow" or "quotes")."""
|
||||
|
||||
normalized = style.strip().lower()
|
||||
if normalized not in {"rainbow", "quotes"}:
|
||||
raise ValueError("Throbber style must be 'rainbow' or 'quotes'.")
|
||||
if normalized == self._style:
|
||||
return
|
||||
self._style = normalized
|
||||
if self._style == "quotes":
|
||||
self._shuffle_quotes()
|
||||
if self._running:
|
||||
self._restart_animation()
|
||||
|
||||
def show_loading(self) -> None:
|
||||
"""Display the throbber and start animation if possible."""
|
||||
|
||||
if self._running:
|
||||
return
|
||||
self._running = True
|
||||
self._apply_visibility(True)
|
||||
self._start_animation()
|
||||
|
||||
def hide_loading(self) -> None:
|
||||
"""Hide the throbber and stop any running animation."""
|
||||
|
||||
if not self._running:
|
||||
return
|
||||
self._running = False
|
||||
self._stop_animation()
|
||||
self.update("")
|
||||
self._apply_visibility(False)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Return whether the throbber is currently animating."""
|
||||
|
||||
return self._running
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Animation helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _restart_animation(self) -> None:
|
||||
self._stop_animation()
|
||||
if self._running:
|
||||
self._start_animation()
|
||||
|
||||
def _start_animation(self) -> None:
|
||||
if self._style == "rainbow":
|
||||
self._render_rainbow_frame()
|
||||
timer_fn = getattr(self, "set_interval", None)
|
||||
if callable(timer_fn):
|
||||
interval = 1.0 / self._RAINBOW_FPS
|
||||
self._rainbow_timer = timer_fn(interval, self._render_rainbow_frame)
|
||||
else:
|
||||
self._advance_quote()
|
||||
timer_fn = getattr(self, "set_interval", None)
|
||||
if callable(timer_fn):
|
||||
self._quotes_timer = timer_fn(
|
||||
self._QUOTES_PERIOD_SECONDS, self._advance_quote
|
||||
)
|
||||
|
||||
def _stop_animation(self) -> None:
|
||||
if self._rainbow_timer is not None:
|
||||
stop = getattr(self._rainbow_timer, "stop", None)
|
||||
if callable(stop): # pragma: no branch - textual timer exposes stop()
|
||||
stop()
|
||||
self._rainbow_timer = None
|
||||
if self._quotes_timer is not None:
|
||||
stop = getattr(self._quotes_timer, "stop", None)
|
||||
if callable(stop):
|
||||
stop()
|
||||
self._quotes_timer = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rendering
|
||||
# ------------------------------------------------------------------
|
||||
def _render_rainbow_frame(self) -> None:
|
||||
"""Render a single rainbow frame and update the widget."""
|
||||
|
||||
frame = self._build_rainbow_frame(self._rainbow_offset)
|
||||
self.update(frame)
|
||||
self._rainbow_offset = (self._rainbow_offset + 1) % len(_RAINBOW_COLORS)
|
||||
|
||||
def _advance_quote(self) -> None:
|
||||
if not self._quotes:
|
||||
return
|
||||
quote = self._quotes[0]
|
||||
self._quotes.rotate(-1)
|
||||
self.update(quote)
|
||||
|
||||
def _build_rainbow_frame(self, offset: int) -> Any:
|
||||
if _RICH_TEXT_CLS is None:
|
||||
# Fallback using plain text with indicators when Rich is unavailable.
|
||||
block = "━" * 30
|
||||
marker = "◆"
|
||||
return f"{block} {marker} {block}"
|
||||
|
||||
text = _RICH_TEXT_CLS()
|
||||
colors = list(_RAINBOW_COLORS)
|
||||
for index in range(len(colors)):
|
||||
color = colors[(index + offset) % len(colors)]
|
||||
text.append("━", style=f"bold {color}")
|
||||
text.append(" ◆ ", style=f"bold {colors[offset % len(colors)]}")
|
||||
for index in range(len(colors) - 1, -1, -1):
|
||||
color = colors[(index + offset) % len(colors)]
|
||||
text.append("━", style=f"bold {color}")
|
||||
return text
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Utilities
|
||||
# ------------------------------------------------------------------
|
||||
def _shuffle_quotes(self) -> None:
|
||||
items = list(self._quotes)
|
||||
self._rng.shuffle(items)
|
||||
self._quotes = deque(items)
|
||||
|
||||
def _apply_visibility(self, is_visible: bool) -> None:
|
||||
target = cast(Any, self)
|
||||
with suppress(Exception):
|
||||
target.display = is_visible
|
||||
with suppress(Exception):
|
||||
target.visible = is_visible
|
||||
styles = getattr(self, "styles", None)
|
||||
if styles is not None: # pragma: no cover - requires Textual runtime
|
||||
if is_visible:
|
||||
styles.height = 1
|
||||
styles.min_height = 1
|
||||
styles.padding = (0, 1)
|
||||
else:
|
||||
styles.height = 0
|
||||
styles.min_height = 0
|
||||
styles.padding = 0
|
||||
|
||||
|
||||
__all__ = ["LoadingThrobber"]
|
||||
Reference in New Issue
Block a user