| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import os
- import random
- import re
- import sys
- import time
- from collections import namedtuple
- from enum import Enum
- from .internal import BARS, SPINNERS, THEMES
- from ..animations.spinners import scrolling_spinner_factory, sequential_spinner_factory
- from ..animations.utils import spinner_player
- from ..core.configuration import config_handler
- from ..utils.cells import print_cells
- from ..utils import terminal
- Show = Enum('Show', 'SPINNERS BARS THEMES')
- def showtime(show=Show.SPINNERS, *, fps=None, length=None, pattern=None):
- """Start a show, rendering all styles simultaneously in your screen.
- Args:
- fps (float): the desired frames per second refresh rate
- show (Show): chooses which show will run
- length (int): the bar length, as in configuration options
- pattern (Pattern): to filter objects displayed
- """
- show_funcs = {
- Show.SPINNERS: show_spinners,
- Show.BARS: show_bars,
- Show.THEMES: show_themes,
- }
- assert show in show_funcs, 'Which show do you want? We have Show.SPINNERS, Show.BARS, and ' \
- 'Show.THEMES.'
- show_funcs[show](fps=fps, length=length, pattern=pattern)
- Info = namedtuple('Info', 'title descr tech')
- def show_spinners(*, fps=None, length=None, pattern=None):
- """Start a spinner show, rendering all styles simultaneously in your screen.
- Args:
- fps (float): the desired frames per second rendition
- length (int): the bar length, as in configuration options
- pattern (Pattern): to filter objects displayed
- """
- selected = _filter(SPINNERS, pattern)
- max_name_length = max(len(s) for s in selected) + 2
- max_natural = max(s.natural for s in selected.values()) + 2
- gens = [_spinner_gen(f'{k:^{max_name_length}}', s, max_natural) for k, s in selected.items()]
- info = Info(
- title=('Spinners', 'including their unknown bar renditions'),
- descr=('Spinners generate and run fluid animations, with a plethora of special effects,'
- ' including static frames, scrolling, bouncing, sequential, alongside or delayed!',
- 'Each type supports several customization options that allow some very cool tricks,'
- ' so be creative 😜'),
- tech=('Spinners are advanced generators that dynamically output frames to generate some '
- 'effect.',
- 'These frames are gathered into full cycles, where the spinner yields. This enables'
- ' to mix and match them, without ever breaking animations.',
- 'All spinners compile their full animations only once before displaying, so they are'
- ' faaaast!',
- 'The spinner compiler brings the super cool `.check()` tool, check it out!',
- 'A spinner have a specific "natural" length, and know how to spread its contents over'
- ' any desired space.',))
- _showtime_gen(fps, gens, info, length)
- def show_bars(*, fps=None, length=None, pattern=None):
- """Start a bar show, rendering all styles simultaneously in your screen.
- Args:
- fps (float): the desired frames per second rendition
- length (int): the bar length, as in configuration options
- pattern (Pattern): to filter objects displayed
- """
- selected = _filter(BARS, pattern)
- max_name_length = max(len(s) for s in selected) + 2
- gens = [_bar_gen(f'{k:>{max_name_length}}', b) for k, b in selected.items()]
- info = Info(
- title=('Bars', 'playing all their hidden tricks'),
- descr=('A bar can render any percentage with a plethora of effects, including dynamic'
- ' chars, tips, backgrounds, transparent fills, underflows and overflows!',
- 'Bars also support some advanced use cases, which do not go only forward...'
- ' Just use manual mode and be creative 😜'),
- tech=('Bars are advanced closures that render percentages with some effect, in a specific'
- ' fixed length.',
- 'Bars are not compiled, but support the super cool `.check()` tool, check it out!',
- 'Furthermore, bars can render any external spinners inside its own borders.'))
- _showtime_gen(fps, gens, info, length)
- def show_themes(*, fps=None, length=None, pattern=None):
- """Start a theme show, rendering all styles simultaneously in your screen.
- Args:
- fps (float): the desired frames per second rendition
- length (int): the bar length, as in configuration options
- pattern (Pattern): to filter objects displayed
- """
- selected = _filter(THEMES, pattern)
- max_name_length = max(len(s) for s in selected) + 2
- themes = {k: config_handler(**v) for k, v in selected.items()}
- max_natural = max(t.spinner.natural for t in themes.values())
- gens = [_theme_gen(f'{k:>{max_name_length}}', c, max_natural) for k, c in themes.items()]
- info = Info(
- title=('Themes', 'featuring their bar, spinner and unknown bar companions'),
- descr=('A theme is an aggregator, it wraps styles that go well together.',),
- tech=('Themes are syntactic sugar, not actually configuration variables (they are elided'
- ' upon usage, only their contents go into the config).',
- 'But you can surely customize them, just send any additional config parameters to'
- ' override anything.'))
- _showtime_gen(fps, gens, info, length)
- def _filter(source, pattern):
- p = re.compile(pattern or '')
- selected = {k: v for k, v in source.items() if p.search(k)}
- if not selected:
- raise ValueError(f'Nothing was selected with pattern "{pattern}".')
- return selected
- # unfortunately these cool infos increase considerably the animations' lengths, and I think it
- # would be ugly to just truncate the animated gifs on the README to try to keep them within
- # reasonable sizes (I can't afford to increase them, since they're very large already).
- # so they will be kind of an easter egg, showing only on users' machines ;)
- _INFO = os.getenv('ALIVE_BAR_EXHIBIT_FULL_INFO', '1') != '0'
- def _showtime_gen(fps, gens, info, length):
- if not sys.stdout.isatty():
- raise UserWarning('This must be run on a tty connected terminal.')
- def title(t, r=False):
- return scrolling_spinner_factory(t, right=r, wrap=False).pause(center=12), # 1-tuple.
- def message(m, s=None):
- return scrolling_spinner_factory(f'{m} 👏, {s}!' if s else m, right=False), # 1-tuple.
- info_spinners = sequential_spinner_factory(
- *(title('Now on stage...')
- + message(*info.title)
- + sum((message(d) for d in info.descr), ())
- + title('Technical details')
- + sum((message(d) for d in info.tech), ())
- + title('Enjoy 🤩', True)),
- intermix=False
- )
- # initialize generators, retrieve their line lengths, and create information line.
- fps, length = min(60., max(2., float(fps or 15.))), length or 40
- cols = max(x for _, x in ((next(gen), gen.send((fps, length))) for gen in gens))
- fps_monitor = 'fps: {:.1f}'
- info_player = spinner_player(info_spinners(max(3, cols - len(fps_monitor.format(fps)) - 1)))
- logo = spinner_player(SPINNERS['waves']())
- start, sleep, frame, line_num = time.perf_counter(), 1. / fps, 0, 0
- start, current = start - sleep, start # simulates the first frame took exactly "sleep" ms.
- term = terminal.get_term()
- term.hide_cursor()
- try:
- while True:
- cols, lines = os.get_terminal_size()
- title = 'Welcome to alive-progress!', ' ', next(logo)
- print_cells(title, cols, term) # line 1.
- term.clear_end_line()
- print()
- info = fps_monitor.format(frame / (current - start)), ' ', next(info_player)
- print_cells(info, cols, term) # line 2.
- term.clear_end_line()
- content = [next(gen) for gen in gens] # always consume gens, to maintain them in sync.
- for line_num, fragments in enumerate(content, 3):
- if line_num > lines:
- break
- print()
- print_cells(fragments, cols, term)
- term.clear_end_line()
- frame += 1
- current = time.perf_counter()
- time.sleep(max(0., start + frame * sleep - current))
- print(f'\x1b[{line_num - 1}A', end='\r') # ANSI escape sequence for Cursor Up.
- except KeyboardInterrupt:
- pass
- finally:
- term.show_cursor()
- def _spinner_gen(name, spinner_factory, max_natural):
- fps, length = yield
- blanks = (' ',) * (max_natural - spinner_factory.natural)
- spinner_gen = exhibit_spinner(spinner_factory())
- unknown_gen = exhibit_spinner(spinner_factory(length))
- yield len(blanks) + spinner_factory.natural + len(name) + length + 4 + 2 # borders/spaces.
- while True:
- yield (blanks, '|', next(spinner_gen), '| ', name, ' |', next(unknown_gen), '|')
- def exhibit_spinner(spinner):
- player = spinner_player(spinner)
- while True:
- yield next(player)
- def _bar_gen(name, bar_factory):
- fps, length = yield
- bar_gen = exhibit_bar(bar_factory(length), fps)
- yield len(name) + length + 2 + 1 # borders/spaces.
- while True:
- yield name, ' ', next(bar_gen)[0]
- def exhibit_bar(bar, fps):
- total = int(fps * 5)
- while True:
- # standard use cases, increment till completion, underflow and overflow.
- for s, t in (0, total), (0, int(total * .5)), (int(total * .5), int(total + 1)):
- for pos in range(s, t):
- percent = pos / total
- yield bar(percent), percent
- # generates a small pause in movement between cases, based on the fps.
- percent = t / total
- for _ in range(int(fps * 2)):
- yield bar.end(percent), percent
- # advanced use cases, which do not go always forward.
- factor = random.random() + 1 # a number between 1.0 and 2.0.
- for percent in (1. - x * factor / total for x in range(total)):
- yield bar(percent), percent
- # generates a small giggle, like a real gauge.
- measure, giggle = random.random(), lambda: (random.random() - .5) * .2
- for _ in range(int(fps * 2)):
- percent = measure + giggle()
- yield bar(percent), percent
- # gradually comes to a full stop.
- for t in range(int(fps * 5)): # smoother stabilization.
- percent = measure + giggle() / 1.04 ** t
- yield bar(percent), percent
- # enjoy the full stop for a while.
- for t in range(int(fps * 2)):
- yield bar(measure), measure
- def _theme_gen(name, config, max_natural):
- fps, length = yield
- bar = config.bar(length, config.unknown)
- bar_std = exhibit_bar(bar, fps)
- bar_unknown = exhibit_bar(bar.unknown, fps)
- blanks = (' ',) * (max_natural - config.spinner.natural)
- spinner = exhibit_spinner(config.spinner())
- yield len(name) + 2 * length + max_natural + 4 + 3 # borders/spaces.
- while True:
- yield (name, ' ', next(bar_std)[0], ' ', next(spinner), blanks, ' ', next(bar_unknown)[0])
|