| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- import math
- from functools import reduce, update_wrapper, wraps
- from inspect import signature
- from itertools import accumulate, chain, repeat
- from typing import Callable
- from ..utils.cells import combine_cells, fix_cells, mark_graphemes, split_graphemes
- def spinner_player(spinner):
- """Create an infinite generator that plays all cycles of a spinner indefinitely."""
- def inner_play():
- while True:
- yield from spinner() # instantiates a new cycle in each iteration.
- return inner_play() # returns an already initiated generator.
- def bordered(borders, default):
- """Decorator to include controllable borders in the outputs of a function."""
- def wrapper(fn):
- @wraps(fn)
- def inner_bordered(*args, **kwargs):
- content, right = fn(*args, **kwargs)
- return combine_cells(left_border, content, right or right_border)
- return inner_bordered
- left_border, right_border = extract_fill_graphemes(borders, default)
- return wrapper
- def extract_fill_graphemes(text, default):
- """Extract the exact same number of graphemes as default, filling missing ones."""
- text, default = (tuple(split_graphemes(c or '') for c in p) for p in (text or default, default))
- return (mark_graphemes(t or d) for t, d in zip(chain(text, repeat('')), default))
- def static_sliding_window(sep, gap, contents, length, right, initial):
- """Implement a sliding window over some content interspersed with a separator.
- It is very efficient, storing data in only one string.
- Note that the implementation is "static" in the sense that the content is pre-
- calculated and maintained static, but actually when the window slides both the
- separator and content seem to be moved.
- Also keep in mind that `right` is for the content, not the window.
- """
- def sliding_window():
- pos = initial
- while True:
- if pos < 0:
- pos += original
- elif pos >= original:
- pos -= original
- yield content[pos:pos + length]
- pos += step
- adjusted_sep = fix_cells((sep * math.ceil(gap / len(sep)))[:gap]) if gap else ''
- content = tuple(chain.from_iterable(chain.from_iterable(zip(repeat(adjusted_sep), contents))))
- original, step = len(content), -1 if right else 1
- assert length <= original, f'window slides inside content, {length} must be <= {original}'
- content += content[:length]
- return sliding_window()
- def overlay_sliding_window(background, gap, contents, length, right, initial):
- """Implement a sliding window over some content on top of a background.
- It uses internally a static sliding window, but dynamically swaps the separator
- characters for the background ones, thus making it appear immobile, with the
- contents sliding over it.
- """
- def overlay_window():
- for cells in window: # pragma: no cover
- yield tuple(b if c == '\0' else c for c, b in zip(cells, background))
- background = (background * math.ceil(length / len(background)))[:length]
- window = static_sliding_window('\0', gap, contents, length, right, initial)
- return overlay_window()
- def combinations(nums):
- """Calculate the number of total combinations a few spinners should have together,
- can be used for example with cycles or with frames played at the same time."""
- def lcm(a, b):
- """Calculate the lowest common multiple of two numbers."""
- return a * b // math.gcd(a, b)
- return reduce(lcm, nums)
- def split_options(options):
- """Split options that apply to dual elements, either duplicating or splitting."""
- return options if isinstance(options, tuple) else (options, options)
- def spread_weighted(actual_length, naturals):
- """Calculate the weighted spreading of the available space for all natural lengths."""
- total = sum(naturals)
- lengths = (actual_length / total * n for n in naturals)
- lengths = [round(x) for x in accumulate(lengths)] # needs to be resolved.
- lengths = tuple(map(lambda a, b: a - b, lengths, [0] + lengths))
- assert sum(lengths) == actual_length
- return lengths
- def fix_signature(func: Callable, source: Callable, skip_n_params: int):
- """Override signature to hide first n parameters."""
- original_doc = func.__doc__
- update_wrapper(func, source)
- if original_doc:
- func.__doc__ = f'{original_doc}\n{func.__doc__}'
- sig = signature(func)
- sig = sig.replace(parameters=tuple(sig.parameters.values())[skip_n_params:])
- func.__signature__ = sig
- return func
- def round_even(n):
- """Round a number to the nearest even integer."""
- r = int(n)
- return r + 1 if r & 1 else r
|