Skip to content

Commit

Permalink
Merge pull request #247 from itsayellow/animate_long_lines
Browse files Browse the repository at this point in the history
Fix animation of long lines
  • Loading branch information
itsayellow authored Oct 29, 2019
2 parents 5d5da98 + 8f89f97 commit 33813d1
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 10 deletions.
35 changes: 25 additions & 10 deletions src/pipx/animate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@
from contextlib import contextmanager
from threading import Event, Thread
from typing import Generator, List
import shutil

from pipx.constants import emoji_support

stderr_is_tty = sys.stderr.isatty()


HIDE_CURSOR = "\033[?25l"
SHOW_CURSOR = "\033[?25h"
CLEAR_LINE = "\033[K"
EMOJI_ANIMATION_FRAMES = ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"]
NONEMOJI_ANIMATION_FRAMES = ["", ".", "..", "..."]
EMOJI_FRAME_PERIOD = 0.1
NONEMOJI_FRAME_PERIOD = 1


@contextmanager
def animate(message: str, do_animation: bool) -> Generator[None, None, None]:

Expand All @@ -20,12 +30,12 @@ def animate(message: str, do_animation: bool) -> Generator[None, None, None]:

if emoji_support:
animate_at_beginning_of_line = True
symbols = ["⣷", "⣯", "⣟", "⡿", "⢿", "⣻", "⣽", "⣾"]
period = 0.1
symbols = EMOJI_ANIMATION_FRAMES
period = EMOJI_FRAME_PERIOD
else:
animate_at_beginning_of_line = False
symbols = ["", ".", "..", "..."]
period = 1
symbols = NONEMOJI_ANIMATION_FRAMES
period = NONEMOJI_FRAME_PERIOD

thread_kwargs = {
"message": message,
Expand Down Expand Up @@ -59,12 +69,17 @@ def print_animation(
period: float,
animate_at_beginning_of_line: bool,
):
(term_cols, _) = shutil.get_terminal_size(fallback=(9999, 24))
while not event.wait(0):
for s in symbols:
if animate_at_beginning_of_line:
cur_line = f"{s} {message}"
max_message_len = term_cols - len(f"{s} ... ")
cur_line = f"{s} {message:.{max_message_len}}"
if len(message) > max_message_len:
cur_line += "..."
else:
cur_line = f"{message}{s}"
max_message_len = term_cols - len("... ")
cur_line = f"{message:.{max_message_len}}{s}"

clear_line()
sys.stderr.write("\r")
Expand All @@ -74,13 +89,13 @@ def print_animation(


def hide_cursor():
sys.stderr.write("\033[?25l")
sys.stderr.write(f"{HIDE_CURSOR}")


def show_cursor():
sys.stderr.write("\033[?25h")
sys.stderr.write(f"{SHOW_CURSOR}")


def clear_line():
sys.stderr.write("\033[K")
sys.stdout.write("\033[K")
sys.stderr.write(f"{CLEAR_LINE}")
sys.stdout.write(f"{CLEAR_LINE}")
76 changes: 76 additions & 0 deletions tests/test_animate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import time

import pipx.animate
from pipx.animate import (
HIDE_CURSOR,
CLEAR_LINE,
EMOJI_ANIMATION_FRAMES,
NONEMOJI_ANIMATION_FRAMES,
EMOJI_FRAME_PERIOD,
NONEMOJI_FRAME_PERIOD,
)


def check_animate_output(
capsys, test_string, frame_strings, frame_period, frames_to_test
):
expected_string = f"{HIDE_CURSOR}" + "".join(frame_strings)

chars_to_test = 1 + len("".join(frame_strings[:frames_to_test]))

with pipx.animate.animate(test_string, do_animation=True):
time.sleep(frame_period * (frames_to_test - 1) + 0.2)
captured = capsys.readouterr()

assert captured.err[:chars_to_test] == expected_string[:chars_to_test]


def test_line_lengths_emoji(capsys, monkeypatch):
# emoji_support and stderr_is_tty is set only at import animate.py
# since we are already after that, we must override both here
monkeypatch.setattr(pipx.animate, "stderr_is_tty", True)
monkeypatch.setattr(pipx.animate, "emoji_support", True)

frames_to_test = 4

# 40-char test_string counts columns e.g.: "0204060810 ... 363840"
test_string = "".join([f"{x:02}" for x in range(2, 41, 2)])

columns_to_test = [45, 46, 47]
expected_message = [f"{test_string:.{45-6}}...", f"{test_string}", f"{test_string}"]

for i, columns in enumerate(columns_to_test):
monkeypatch.setenv("COLUMNS", str(columns))

frame_strings = [
f"{CLEAR_LINE}\r{x} {expected_message[i]}" for x in EMOJI_ANIMATION_FRAMES
]
check_animate_output(
capsys, test_string, frame_strings, EMOJI_FRAME_PERIOD, frames_to_test
)


def test_line_lengths_no_emoji(capsys, monkeypatch):
# emoji_support and stderr_is_tty is set only at import animate.py
# since we are already after that, we must override both here
monkeypatch.setattr(pipx.animate, "stderr_is_tty", True)
monkeypatch.setattr(pipx.animate, "emoji_support", False)

frames_to_test = 2

# 40-char test_string counts columns e.g.: "0204060810 ... 363840"
test_string = "".join([f"{x:02}" for x in range(2, 41, 2)])

columns_to_test = [43, 44, 45]
expected_message = [f"{test_string:.{43-4}}", f"{test_string}", f"{test_string}"]

for i, columns in enumerate(columns_to_test):
monkeypatch.setenv("COLUMNS", str(columns))

frame_strings = [
f"{CLEAR_LINE}\r{expected_message[i]}{x}" for x in NONEMOJI_ANIMATION_FRAMES
]

check_animate_output(
capsys, test_string, frame_strings, NONEMOJI_FRAME_PERIOD, frames_to_test
)

0 comments on commit 33813d1

Please sign in to comment.