Fix spinner spam in non-TTY mode (Claude Code)
- Detect if running in real terminal vs captured output - In TTY: show animated spinner with carriage returns - In non-TTY: print simple static messages (no animation) - Prevents output file from exploding with spinner frames Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+36
-11
@@ -1,11 +1,15 @@
|
|||||||
"""Terminal UI utilities for last30days skill."""
|
"""Terminal UI utilities for last30days skill."""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import random
|
import random
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
# Check if we're in a real terminal (not captured by Claude Code)
|
||||||
|
IS_TTY = sys.stderr.isatty()
|
||||||
|
|
||||||
# ANSI color codes
|
# ANSI color codes
|
||||||
class Colors:
|
class Colors:
|
||||||
PURPLE = '\033[95m'
|
PURPLE = '\033[95m'
|
||||||
@@ -82,6 +86,7 @@ class Spinner:
|
|||||||
self.running = False
|
self.running = False
|
||||||
self.thread: Optional[threading.Thread] = None
|
self.thread: Optional[threading.Thread] = None
|
||||||
self.frame_idx = 0
|
self.frame_idx = 0
|
||||||
|
self.shown_static = False
|
||||||
|
|
||||||
def _spin(self):
|
def _spin(self):
|
||||||
while self.running:
|
while self.running:
|
||||||
@@ -93,20 +98,33 @@ class Spinner:
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.running = True
|
self.running = True
|
||||||
self.thread = threading.Thread(target=self._spin, daemon=True)
|
if IS_TTY:
|
||||||
self.thread.start()
|
# Real terminal - animate
|
||||||
|
self.thread = threading.Thread(target=self._spin, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
else:
|
||||||
|
# Not a TTY (Claude Code) - just print once
|
||||||
|
if not self.shown_static:
|
||||||
|
sys.stderr.write(f"⏳ {self.message}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
self.shown_static = True
|
||||||
|
|
||||||
def update(self, message: str):
|
def update(self, message: str):
|
||||||
self.message = message
|
self.message = message
|
||||||
|
if not IS_TTY and not self.shown_static:
|
||||||
|
# Print update in non-TTY mode
|
||||||
|
sys.stderr.write(f"⏳ {message}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
def stop(self, final_message: str = ""):
|
def stop(self, final_message: str = ""):
|
||||||
self.running = False
|
self.running = False
|
||||||
if self.thread:
|
if self.thread:
|
||||||
self.thread.join(timeout=0.2)
|
self.thread.join(timeout=0.2)
|
||||||
# Clear the line
|
if IS_TTY:
|
||||||
sys.stderr.write("\r" + " " * 80 + "\r")
|
# Clear the line in real terminal
|
||||||
|
sys.stderr.write("\r" + " " * 80 + "\r")
|
||||||
if final_message:
|
if final_message:
|
||||||
sys.stderr.write(f"{Colors.GREEN}✓{Colors.RESET} {final_message}\n")
|
sys.stderr.write(f"✓ {final_message}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
@@ -122,8 +140,12 @@ class ProgressDisplay:
|
|||||||
self._show_banner()
|
self._show_banner()
|
||||||
|
|
||||||
def _show_banner(self):
|
def _show_banner(self):
|
||||||
sys.stderr.write(MINI_BANNER + "\n")
|
if IS_TTY:
|
||||||
sys.stderr.write(f"{Colors.DIM}Topic: {Colors.RESET}{Colors.BOLD}{self.topic}{Colors.RESET}\n\n")
|
sys.stderr.write(MINI_BANNER + "\n")
|
||||||
|
sys.stderr.write(f"{Colors.DIM}Topic: {Colors.RESET}{Colors.BOLD}{self.topic}{Colors.RESET}\n\n")
|
||||||
|
else:
|
||||||
|
# Simple text for non-TTY
|
||||||
|
sys.stderr.write(f"/last30days · researching: {self.topic}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def start_reddit(self):
|
def start_reddit(self):
|
||||||
@@ -171,10 +193,13 @@ class ProgressDisplay:
|
|||||||
|
|
||||||
def show_complete(self, reddit_count: int, x_count: int):
|
def show_complete(self, reddit_count: int, x_count: int):
|
||||||
elapsed = time.time() - self.start_time
|
elapsed = time.time() - self.start_time
|
||||||
sys.stderr.write(f"\n{Colors.GREEN}{Colors.BOLD}✓ Research complete{Colors.RESET} ")
|
if IS_TTY:
|
||||||
sys.stderr.write(f"{Colors.DIM}({elapsed:.1f}s){Colors.RESET}\n")
|
sys.stderr.write(f"\n{Colors.GREEN}{Colors.BOLD}✓ Research complete{Colors.RESET} ")
|
||||||
sys.stderr.write(f" {Colors.YELLOW}Reddit:{Colors.RESET} {reddit_count} threads ")
|
sys.stderr.write(f"{Colors.DIM}({elapsed:.1f}s){Colors.RESET}\n")
|
||||||
sys.stderr.write(f"{Colors.CYAN}X:{Colors.RESET} {x_count} posts\n\n")
|
sys.stderr.write(f" {Colors.YELLOW}Reddit:{Colors.RESET} {reddit_count} threads ")
|
||||||
|
sys.stderr.write(f"{Colors.CYAN}X:{Colors.RESET} {x_count} posts\n\n")
|
||||||
|
else:
|
||||||
|
sys.stderr.write(f"✓ Research complete ({elapsed:.1f}s) - Reddit: {reddit_count} threads, X: {x_count} posts\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def show_cached(self, age_hours: float = None):
|
def show_cached(self, age_hours: float = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user