177 lines
5.5 KiB
Python
177 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""A minimal non-colored version of https://pypi.org/project/halo, to track list progress"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import os
|
|
import sys
|
|
import threading
|
|
from collections import OrderedDict
|
|
from datetime import datetime
|
|
|
|
import py
|
|
|
|
threads = []
|
|
|
|
if os.name == "nt":
|
|
import ctypes
|
|
|
|
class _CursorInfo(ctypes.Structure):
|
|
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
|
|
|
|
|
|
def _file_support_encoding(chars, file):
|
|
encoding = getattr(file, "encoding", None)
|
|
if encoding is not None:
|
|
for char in chars:
|
|
try:
|
|
char.encode(encoding)
|
|
except UnicodeEncodeError:
|
|
break
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
|
|
class Spinner(object):
|
|
CLEAR_LINE = "\033[K"
|
|
max_width = 120
|
|
UNICODE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
ASCII_FRAMES = ["|", "-", "+", "x", "*"]
|
|
|
|
def __init__(self, enabled=True, refresh_rate=0.1):
|
|
self.refresh_rate = refresh_rate
|
|
self.enabled = enabled
|
|
self._file = sys.stdout
|
|
self.frames = (
|
|
self.UNICODE_FRAMES
|
|
if _file_support_encoding(self.UNICODE_FRAMES, sys.stdout)
|
|
else self.ASCII_FRAMES
|
|
)
|
|
self.stream = py.io.TerminalWriter(file=self._file)
|
|
self._envs = OrderedDict()
|
|
self._frame_index = 0
|
|
|
|
def clear(self):
|
|
if self.enabled:
|
|
self.stream.write("\r")
|
|
self.stream.write(self.CLEAR_LINE)
|
|
|
|
def render(self):
|
|
while True:
|
|
self._stop_spinner.wait(self.refresh_rate)
|
|
if self._stop_spinner.is_set():
|
|
break
|
|
self.render_frame()
|
|
return self
|
|
|
|
def render_frame(self):
|
|
if self.enabled:
|
|
self.clear()
|
|
self.stream.write("\r{}".format(self.frame()))
|
|
|
|
def frame(self):
|
|
frame = self.frames[self._frame_index]
|
|
self._frame_index += 1
|
|
self._frame_index = self._frame_index % len(self.frames)
|
|
text_frame = "[{}] {}".format(len(self._envs), " | ".join(self._envs))
|
|
if len(text_frame) > self.max_width - 1:
|
|
text_frame = "{}...".format(text_frame[: self.max_width - 1 - 3])
|
|
return "{} {}".format(*[(frame, text_frame)][0])
|
|
|
|
def __enter__(self):
|
|
if self.enabled:
|
|
self.disable_cursor()
|
|
self.render_frame()
|
|
self._stop_spinner = threading.Event()
|
|
self._spinner_thread = threading.Thread(target=self.render)
|
|
self._spinner_thread.setDaemon(True)
|
|
self._spinner_thread.start()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if not self._stop_spinner.is_set():
|
|
if self._spinner_thread:
|
|
self._stop_spinner.set()
|
|
self._spinner_thread.join()
|
|
|
|
self._frame_index = 0
|
|
if self.enabled:
|
|
self.clear()
|
|
self.enable_cursor()
|
|
|
|
return self
|
|
|
|
def add(self, name):
|
|
self._envs[name] = datetime.now()
|
|
|
|
def succeed(self, key):
|
|
self.finalize(key, "✔ OK", green=True)
|
|
|
|
def fail(self, key):
|
|
self.finalize(key, "✖ FAIL", red=True)
|
|
|
|
def skip(self, key):
|
|
self.finalize(key, "⚠ SKIP", white=True)
|
|
|
|
def finalize(self, key, status, **kwargs):
|
|
start_at = self._envs[key]
|
|
del self._envs[key]
|
|
if self.enabled:
|
|
self.clear()
|
|
self.stream.write(
|
|
"{} {} in {}{}".format(
|
|
status,
|
|
key,
|
|
td_human_readable(datetime.now() - start_at),
|
|
os.linesep,
|
|
),
|
|
**kwargs
|
|
)
|
|
if not self._envs:
|
|
self.__exit__(None, None, None)
|
|
|
|
def disable_cursor(self):
|
|
if self._file.isatty():
|
|
if os.name == "nt":
|
|
ci = _CursorInfo()
|
|
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
|
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
|
|
ci.visible = False
|
|
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
|
elif os.name == "posix":
|
|
self.stream.write("\033[?25l")
|
|
|
|
def enable_cursor(self):
|
|
if self._file.isatty():
|
|
if os.name == "nt":
|
|
ci = _CursorInfo()
|
|
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
|
ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci))
|
|
ci.visible = True
|
|
ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci))
|
|
elif os.name == "posix":
|
|
self.stream.write("\033[?25h")
|
|
|
|
|
|
def td_human_readable(delta):
|
|
seconds = int(delta.total_seconds())
|
|
periods = [
|
|
("year", 60 * 60 * 24 * 365),
|
|
("month", 60 * 60 * 24 * 30),
|
|
("day", 60 * 60 * 24),
|
|
("hour", 60 * 60),
|
|
("minute", 60),
|
|
("second", 1),
|
|
]
|
|
|
|
texts = []
|
|
for period_name, period_seconds in periods:
|
|
if seconds > period_seconds or period_seconds == 1:
|
|
period_value, seconds = divmod(seconds, period_seconds)
|
|
if period_name == "second":
|
|
ms = delta.total_seconds() - int(delta.total_seconds())
|
|
period_value = round(period_value + ms, 3)
|
|
has_s = "s" if period_value != 1 else ""
|
|
texts.append("{} {}{}".format(period_value, period_name, has_s))
|
|
return ", ".join(texts)
|