From 21a05ead4ef637df0cc883d4780b92bd4bccafde Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 15:33:19 -0700 Subject: [PATCH] refactor: Extract WaitingSpinner to its own module --- aider/mdstream.py | 26 ++++++++++------------- aider/waiting.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/aider/mdstream.py b/aider/mdstream.py index 8820d7261..0611a806d 100755 --- a/aider/mdstream.py +++ b/aider/mdstream.py @@ -13,7 +13,7 @@ from rich.syntax import Syntax from rich.text import Text from aider.dump import dump # noqa: F401 -from aider.utils import Spinner +from aider.waiting import WaitingSpinner _text_prefix = """ # Header @@ -117,12 +117,11 @@ class MarkdownStream: else: self.mdargs = dict() - # Defer Live creation until the first update so the Spinner can be shown. + # Defer Live creation until the first update so the spinner can be shown. self.live = None - self.spinner = Spinner("Waiting for LLM") - self._spinner_stop_event = threading.Event() - self._spinner_thread = threading.Thread(target=self._spin, daemon=True) - self._spinner_thread.start() + self.waiting_spinner = WaitingSpinner("Waiting for LLM") + self.waiting_spinner.start() + self._spinner_running = True self._live_started = False def _spin(self): @@ -159,12 +158,10 @@ class MarkdownStream: except Exception: pass # Ignore any errors during cleanup - # Ensure the spinner thread is properly shut down - if hasattr(self, "_spinner_stop_event"): - self._spinner_stop_event.set() - if hasattr(self, "_spinner_thread") and self._spinner_thread.is_alive(): + # Ensure the spinner is properly shut down + if getattr(self, "_spinner_running", False): try: - self._spinner_thread.join(timeout=0.1) + self.waiting_spinner.stop() except Exception: pass @@ -187,10 +184,9 @@ class MarkdownStream: """ # On the first call, stop the spinner and start the Live renderer if not getattr(self, "_live_started", False): - if hasattr(self, "_spinner_stop_event"): - self._spinner_stop_event.set() - if hasattr(self, "_spinner_thread"): - self._spinner_thread.join() + if getattr(self, "_spinner_running", False): + self.waiting_spinner.stop() + self._spinner_running = False self.live = Live(Text(""), refresh_per_second=1.0 / self.min_delay) self.live.start() self._live_started = True diff --git a/aider/waiting.py b/aider/waiting.py index e69de29bb..c0b121e74 100644 --- a/aider/waiting.py +++ b/aider/waiting.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" +Thread-based, killable spinner utility. + +Use it like: + + from aider.waiting import WaitingSpinner + + spinner = WaitingSpinner("Waiting for LLM") + spinner.start() + ... # long task + spinner.stop() +""" + +import threading +import time + +from aider.utils import Spinner + + +class WaitingSpinner: + """Background spinner that can be started/stopped safely.""" + + def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15): + self.spinner = Spinner(text) + self.delay = delay + self._stop_event = threading.Event() + self._thread = threading.Thread(target=self._spin, daemon=True) + + def _spin(self): + while not self._stop_event.is_set(): + time.sleep(self.delay) + self.spinner.step() + self.spinner.end() + + def start(self): + """Start the spinner in a background thread.""" + if not self._thread.is_alive(): + self._thread.start() + + def stop(self): + """Request the spinner to stop and wait briefly for the thread to exit.""" + self._stop_event.set() + if self._thread.is_alive(): + self._thread.join(timeout=0.1) + + # Allow use as a context-manager + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop()