Skip to content

Commit

Permalink
Merge fcf11be into e773f3b
Browse files Browse the repository at this point in the history
  • Loading branch information
manrajgrover committed Aug 1, 2020
2 parents e773f3b + fcf11be commit 05e1806
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 71 deletions.
48 changes: 48 additions & 0 deletions halo/cursor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Source: https://stackoverflow.com/a/10455937/2692667
"""

import sys
import os

if os.name == "nt":
import ctypes

class _CursorInfo(ctypes.Structure):
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]


def hide_cursor(stream=sys.stdout):
"""Hide cursor.
Parameters
----------
stream: sys.stdout, Optional
Defines stream to write output to.
"""
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":
stream.write("\033[?25l")
stream.flush()


def show_cursor(stream=sys.stdout):
"""Show cursor.
Parameters
----------
stream: sys.stdout, Optional
Defines stream to write output to.
"""
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":
stream.write("\033[?25h")
stream.flush()
116 changes: 69 additions & 47 deletions halo/halo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@
import threading
import time

import cursor
import halo.cursor as cursor

from log_symbols.symbols import LogSymbols
from spinners.spinners import Spinners

from halo._utils import (colored_frame, decode_utf_8_text, get_environment,
get_terminal_columns, is_supported, is_text_type,
encode_utf_8_text)
from halo._utils import (
colored_frame,
decode_utf_8_text,
get_environment,
get_terminal_columns,
is_supported,
is_text_type,
encode_utf_8_text,
)


class Halo(object):
Expand All @@ -27,11 +34,24 @@ class Halo(object):
Code to clear the line
"""

CLEAR_LINE = '\033[K'
SPINNER_PLACEMENTS = ('left', 'right',)

def __init__(self, text='', color='cyan', text_color=None, spinner=None,
animation=None, placement='left', interval=-1, enabled=True, stream=sys.stdout):
CLEAR_LINE = "\033[K"
SPINNER_PLACEMENTS = (
"left",
"right",
)

def __init__(
self,
text="",
color="cyan",
text_color=None,
spinner=None,
animation=None,
placement="left",
interval=-1,
enabled=True,
stream=sys.stdout,
):
"""Constructs the Halo object.
Parameters
----------
Expand Down Expand Up @@ -64,7 +84,9 @@ def __init__(self, text='', color='cyan', text_color=None, spinner=None,
self.text = text
self._text_color = text_color

self._interval = int(interval) if int(interval) > 0 else self._spinner['interval']
self._interval = (
int(interval) if int(interval) > 0 else self._spinner["interval"]
)
self._stream = stream

self.placement = placement
Expand All @@ -81,11 +103,11 @@ def clean_up():
"""Handle cell execution"""
self.stop()

if environment in ('ipython', 'jupyter'):
if environment in ("ipython", "jupyter"):
from IPython import get_ipython

ip = get_ipython()
ip.events.register('post_run_cell', clean_up)
ip.events.register("post_run_cell", clean_up)
else: # default terminal
atexit.register(clean_up)

Expand Down Expand Up @@ -142,7 +164,7 @@ def text(self):
str
text value
"""
return self._text['original']
return self._text["original"]

@text.setter
def text(self, text):
Expand Down Expand Up @@ -214,7 +236,10 @@ def placement(self, placement):
"""
if placement not in self.SPINNER_PLACEMENTS:
raise ValueError(
"Unknown spinner placement '{0}', available are {1}".format(placement, self.SPINNER_PLACEMENTS))
"Unknown spinner placement '{0}', available are {1}".format(
placement, self.SPINNER_PLACEMENTS
)
)
self._placement = placement

@property
Expand Down Expand Up @@ -246,7 +271,7 @@ def animation(self, animation):
Defines the animation of the spinner
"""
self._animation = animation
self._text = self._get_text(self._text['original'])
self._text = self._get_text(self._text["original"])

def _check_stream(self):
"""Returns whether the stream is open, and if applicable, writable
Expand Down Expand Up @@ -303,7 +328,7 @@ def _get_spinner(self, spinner):
dict
Contains frames and interval defining spinner
"""
default_spinner = Spinners['dots'].value
default_spinner = Spinners["dots"].value

if spinner and type(spinner) == dict:
return spinner
Expand All @@ -314,7 +339,7 @@ def _get_spinner(self, spinner):
else:
return default_spinner
else:
return Spinners['line'].value
return Spinners["line"].value

def _get_text(self, text):
"""Creates frames based on the selected animation
Expand All @@ -326,7 +351,7 @@ def _get_text(self, text):
stripped_text = text.strip()

# Check which frame of the animation is the widest
max_spinner_length = max([len(i) for i in self._spinner['frames']])
max_spinner_length = max([len(i) for i in self._spinner["frames"]])

# Subtract to the current terminal size the max spinner length
# (-1 to leave room for the extra space between spinner and text)
Expand All @@ -336,30 +361,27 @@ def _get_text(self, text):
frames = []

if terminal_width < text_length and animation:
if animation == 'bounce':
if animation == "bounce":
"""
Make the text bounce back and forth
"""
for x in range(0, text_length - terminal_width + 1):
frames.append(stripped_text[x:terminal_width + x])
frames.append(stripped_text[x : terminal_width + x])
frames.extend(list(reversed(frames)))
elif 'marquee':
elif "marquee":
"""
Make the text scroll like a marquee
"""
stripped_text = stripped_text + ' ' + stripped_text[:terminal_width]
stripped_text = stripped_text + " " + stripped_text[:terminal_width]
for x in range(0, text_length + 1):
frames.append(stripped_text[x:terminal_width + x])
frames.append(stripped_text[x : terminal_width + x])
elif terminal_width < text_length and not animation:
# Add ellipsis if text is larger than terminal width and no animation was specified
frames = [stripped_text[:terminal_width - 6] + ' (...)']
frames = [stripped_text[: terminal_width - 6] + " (...)"]
else:
frames = [stripped_text]

return {
'original': text,
'frames': frames
}
return {"original": text, "frames": frames}

def clear(self):
"""Clears the line and returns cursor to the start.
Expand All @@ -368,7 +390,7 @@ def clear(self):
-------
self
"""
self._write('\r')
self._write("\r")
self._write(self.CLEAR_LINE)
return self

Expand All @@ -383,7 +405,7 @@ def _render_frame(self):

self.clear()
frame = self.frame()
output = '\r{}'.format(frame)
output = "\r{}".format(frame)
try:
self._write(output)
except UnicodeEncodeError:
Expand All @@ -407,7 +429,7 @@ def frame(self):
-------
self
"""
frames = self._spinner['frames']
frames = self._spinner["frames"]
frame = frames[self._frame_index]

if self._color:
Expand All @@ -417,26 +439,28 @@ def frame(self):
self._frame_index = self._frame_index % len(frames)

text_frame = self.text_frame()
return u'{0} {1}'.format(*[
(text_frame, frame)
if self._placement == 'right' else
(frame, text_frame)
][0])
return "{0} {1}".format(
*[
(text_frame, frame)
if self._placement == "right"
else (frame, text_frame)
][0]
)

def text_frame(self):
"""Builds and returns the text frame to be rendered
Returns
-------
self
"""
if len(self._text['frames']) == 1:
if len(self._text["frames"]) == 1:
if self._text_color:
return colored_frame(self._text['frames'][0], self._text_color)
return colored_frame(self._text["frames"][0], self._text_color)

# Return first frame (can't return original text because at this point it might be ellipsed)
return self._text['frames'][0]
return self._text["frames"][0]

frames = self._text['frames']
frames = self._text["frames"]
frame = frames[self._text_index]

self._text_index += 1
Expand Down Expand Up @@ -543,7 +567,7 @@ def info(self, text=None):
"""
return self.stop_and_persist(symbol=LogSymbols.INFO.value, text=text)

def stop_and_persist(self, symbol=' ', text=None):
def stop_and_persist(self, symbol=" ", text=None):
"""Stops the spinner and persists the final frame to be shown.
Parameters
----------
Expand All @@ -564,7 +588,7 @@ def stop_and_persist(self, symbol=' ', text=None):
if text is not None:
text = decode_utf_8_text(text)
else:
text = self._text['original']
text = self._text["original"]

text = text.strip()

Expand All @@ -573,11 +597,9 @@ def stop_and_persist(self, symbol=' ', text=None):

self.stop()

output = u'{0} {1}\n'.format(*[
(text, symbol)
if self._placement == 'right' else
(symbol, text)
][0])
output = "{0} {1}\n".format(
*[(text, symbol) if self._placement == "right" else (symbol, text)][0]
)

try:
self._write(output)
Expand Down

0 comments on commit 05e1806

Please sign in to comment.