Adds console task utility with spinner and log management
Introduces a utility class for managing console tasks with a spinner and log output. Includes features for logging messages, running commands, and displaying real-time status updates. Enhances user experience with clear task status visualization.
This commit is contained in:
123
src/utils/console_utils.py
Normal file
123
src/utils/console_utils.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
from typing import List, Optional
|
||||
|
||||
class ConsoleTask:
|
||||
def __init__(self, title: str, max_log_lines: int = 10):
|
||||
self.title = title
|
||||
self.max_log_lines = max_log_lines
|
||||
self.spinner_cycle = ['|', '/', '-', '\\']
|
||||
self.stop_event = threading.Event()
|
||||
self.spinner_thread: Optional[threading.Thread] = None
|
||||
self.output_lines: List[str] = []
|
||||
self._stdout_lock = threading.Lock()
|
||||
self._drawn_lines = 0 # Track only real drawn lines, no pre-reservation anymore
|
||||
|
||||
def start_spinner(self):
|
||||
self.spinner_thread = threading.Thread(target=self._spinner_task, daemon=True)
|
||||
self.spinner_thread.start()
|
||||
|
||||
def _spinner_task(self):
|
||||
idx = 0
|
||||
while not self.stop_event.is_set():
|
||||
with self._stdout_lock:
|
||||
self._redraw_spinner(idx)
|
||||
idx += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
def _redraw_spinner(self, idx: int):
|
||||
visible_lines = self.output_lines[-self.max_log_lines:]
|
||||
|
||||
# Clear only previously drawn lines
|
||||
if self._drawn_lines > 0:
|
||||
sys.stdout.write(f"\033[{self._drawn_lines}F") # Move cursor up
|
||||
|
||||
for _ in range(self._drawn_lines):
|
||||
sys.stdout.write("\r\033[K") # Clear line
|
||||
sys.stdout.write("\033[1B") # Move cursor one line down
|
||||
|
||||
sys.stdout.write(f"\033[{self._drawn_lines}F") # Move cursor up to redraw start
|
||||
|
||||
# Draw fresh content
|
||||
self._drawn_lines = 0 # Reset counter
|
||||
|
||||
# Draw spinner line
|
||||
sys.stdout.write(f"\r{self.title} {self.spinner_cycle[idx % len(self.spinner_cycle)]}\n")
|
||||
self._drawn_lines += 1
|
||||
|
||||
# Draw log lines
|
||||
for line in visible_lines:
|
||||
sys.stdout.write(line + "\n")
|
||||
self._drawn_lines += 1
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
def log(self, message: str):
|
||||
with self._stdout_lock:
|
||||
self.output_lines.append(message)
|
||||
if len(self.output_lines) > self.max_log_lines:
|
||||
self.output_lines = self.output_lines[-self.max_log_lines:]
|
||||
|
||||
def run_command(self, cmd: List[str], cwd: Optional[str] = None, silent: bool = False) -> int:
|
||||
success = False
|
||||
start_time = time.time()
|
||||
|
||||
self.start_spinner()
|
||||
|
||||
try:
|
||||
if silent:
|
||||
subprocess.run(cmd, cwd=cwd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
success = True
|
||||
else:
|
||||
process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if process.stdout is None:
|
||||
raise ValueError("Failed to capture stdout")
|
||||
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
self.log(line.rstrip())
|
||||
|
||||
success = (process.returncode == 0)
|
||||
if not success:
|
||||
raise subprocess.CalledProcessError(process.returncode, cmd)
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
success = False
|
||||
raise
|
||||
finally:
|
||||
self.stop_event.set()
|
||||
if self.spinner_thread:
|
||||
self.spinner_thread.join()
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Final redraw after stop
|
||||
with self._stdout_lock:
|
||||
self._finalize_output(success, duration)
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
def _finalize_output(self, success: bool, duration: float):
|
||||
visible_lines = self.output_lines[-self.max_log_lines:]
|
||||
|
||||
# Clear last drawn area
|
||||
if self._drawn_lines > 0:
|
||||
sys.stdout.write(f"\033[{self._drawn_lines}F")
|
||||
for _ in range(self._drawn_lines):
|
||||
sys.stdout.write("\r\033[K")
|
||||
sys.stdout.write("\033[1B")
|
||||
sys.stdout.write(f"\033[{self._drawn_lines}F")
|
||||
|
||||
self._drawn_lines = 0 # Reset
|
||||
|
||||
# Write final status line
|
||||
status = "\033[92m✅\033[0m" if success else "\033[91m❌\033[0m"
|
||||
sys.stdout.write(f"\r{self.title} {status} ({duration:.1f}s)\n")
|
||||
self._drawn_lines += 1
|
||||
|
||||
sys.stdout.flush()
|
Reference in New Issue
Block a user