dotfiles/bin/gay

386 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
from argparse import ArgumentParser, Namespace
from atexit import register
from enum import Enum
from itertools import chain, count, cycle, islice, repeat
from math import pi, sin
from os import environ
from platform import system
from random import choice, randint
from shutil import get_terminal_size
from sys import stdin
from typing import Callable, Dict, Iterator, List, Optional, Tuple, cast
from unicodedata import east_asian_width
class ColourSpace(Enum):
EIGHT = "8"
TRUE = "24"
class Gradient(Enum):
D1 = "1d"
D2 = "2d"
class Flags(Enum):
LES = 1
GAY = 2
BI = 3
TRANS = 4
ACE = 5
PAN = 6
NB = 7
GQ = 8
HexColour = str
RGB = Tuple[int, int, int]
RawPalette = List[HexColour]
Palette = List[RGB]
WINDOWS = system() == "Windows"
COLS, ROWS = get_terminal_size((80, 80))
UNICODE_WIDTH_LOOKUP = {
"W": 2, # CJK
"N": 0, # Non printable
}
ASPECT_RATIO = (3, 5)
FLAG_SPECS: Dict[Flags, RawPalette] = {
Flags.LES: ["#D62E02", "#FD9855", "#FFFFFF", "#D161A2", "#A20160"],
Flags.GAY: ["#FF0018", "#FFA52C", "#FFFF41", "#008018", "#0000F9", "#86007D"],
Flags.BI: ["#D60270", "#D60270", "#9B4F96", "#0038A8", "#0038A8"],
Flags.TRANS: ["#55CDFC", "#F7A8B8", "#FFFFFF", "#F7A8B8", "#55CDFC"],
Flags.ACE: ["#000000", "#A4A4A4", "#FFFFFF", "#810081"],
Flags.PAN: ["#FF1B8D", "#FFDA00", "#1BB3FF"],
Flags.NB: ["#FFF430", "#FFFFFF", "#9C59D1", "#000000"],
Flags.GQ: ["#B77FDD", "#FFFFFF", "#48821E"],
}
def parse_args() -> Namespace:
rand_flag = choice(tuple(f for f in Flags))
rand_interpolation = choice(tuple(i.value for i in Gradient))
colour_space = (
ColourSpace.TRUE
if environ.get("COLORTERM") in {"truecolor", "24bit"}
else ColourSpace.EIGHT
)
namespace = Namespace(flag=rand_flag)
parser = ArgumentParser()
mode_group = parser.add_argument_group()
mode_group.add_argument("-f", "--flag", dest="flag_only", action="store_true")
flag_group = parser.add_argument_group()
flag_group.add_argument(
"-l", "--les", "--lesbian", action="store_const", dest="flag", const=Flags.LES,
)
flag_group.add_argument(
"-g", "--gay", action="store_const", dest="flag", const=Flags.GAY,
)
flag_group.add_argument(
"-b", "--bi", "--bisexual", action="store_const", dest="flag", const=Flags.BI,
)
flag_group.add_argument(
"-t",
"--trans",
"--transgender",
action="store_const",
dest="flag",
const=Flags.TRANS,
)
flag_group.add_argument(
"-a", "--ace", "--asexual", action="store_const", dest="flag", const=Flags.ACE,
)
flag_group.add_argument(
"-p",
"--pan",
"--pansexual",
action="store_const",
dest="flag",
const=Flags.PAN,
)
flag_group.add_argument(
"-n", "--nb", "--non-binary", action="store_const", dest="flag", const=Flags.NB,
)
flag_group.add_argument(
"--gq", "--gender-queer", action="store_const", dest="flag", const=Flags.GQ,
)
opt_group = parser.add_argument_group()
opt_group.add_argument(
"-c",
"--colour",
choices=tuple(c.value for c in ColourSpace),
default=colour_space.value,
)
opt_group.add_argument(
"-i",
"--interpolation",
choices=tuple(i.value for i in Gradient),
default=rand_interpolation,
)
opt_group.add_argument("--period", type=lambda i: max(abs(int(i)), 1))
opt_group.add_argument(
"--tabs", "--tab-width", type=lambda i: max(abs(int(i)), 1), default=4
)
return parser.parse_args(namespace=namespace)
def trap_sig() -> None:
if not WINDOWS:
from signal import SIG_DFL, SIGPIPE, signal
signal(SIGPIPE, SIG_DFL)
def on_exit() -> None:
print("\033[0m", end="", flush=True)
def stdin_stream() -> Iterator[str]:
while True:
size = COLS * 4
line = stdin.read(size)
if line:
yield from line
else:
break
def normalize_width(tab_width: int, stream: Iterator[str]) -> Iterator[str]:
for char in stream:
if char == "\t":
yield from repeat(" ", tab_width)
else:
yield char
def unicode_width(stream: Iterator[str]) -> Iterator[Tuple[int, str]]:
def char_width(char: str) -> int:
try:
code = east_asian_width(char)
return UNICODE_WIDTH_LOOKUP.get(code, 1)
except Exception:
return 1
for char in stream:
yield char_width(char), char
def parse_raw_palette(raw: RawPalette) -> Palette:
def parse_colour(colour: HexColour) -> RGB:
hexc = colour[1:]
it = iter(hexc)
parsed = tuple(
int(f"{h1}{h2}", 16) for h1, h2 in iter(lambda: tuple(islice(it, 2)), ())
)
return cast(RGB, parsed)
return [parse_colour(p) for p in raw]
DecorateChar = Callable[[RGB], Iterator[str]]
DecorateReset = Callable[[], Iterator[str]]
def decor_for(space: ColourSpace) -> Tuple[DecorateChar, DecorateChar, DecorateReset]:
def reset() -> Iterator[str]:
yield "\033[0m"
def decor_8(rgb: RGB) -> Iterator[str]:
r, g, b = map(lambda c: int(round(c / 255 * 5)), rgb)
yield ";"
yield str(16 + 36 * r + 6 * g + b)
def decor_24(rgb: RGB) -> Iterator[str]:
yield from chain(*zip(repeat(";"), map(str, rgb)))
if space == ColourSpace.EIGHT:
def fg(colour: RGB) -> Iterator[str]:
yield "\033[38;5"
yield from decor_8(colour)
yield "m"
def bg(colour: RGB) -> Iterator[str]:
yield "\033[48;5"
yield from decor_8(colour)
yield "m"
return fg, bg, reset
elif space == ColourSpace.TRUE:
def fg(colour: RGB) -> Iterator[str]:
yield "\033[38;2"
yield from decor_24(colour)
yield "m"
def bg(colour: RGB) -> Iterator[str]:
yield "\033[48;2"
yield from decor_24(colour)
yield "m"
return fg, bg, reset
else:
raise ValueError()
def paint_flag(colour_space: ColourSpace, palette: Palette) -> Iterator[str]:
r, c = ASPECT_RATIO
_, bg, reset = decor_for(colour_space)
height = len(palette)
ratio = r / c * 0.5
multiplier = max(int(min((ROWS - 4) / height, COLS / height * ratio)), 1)
line = " " * COLS
for colour in palette:
for _ in range(multiplier):
yield from bg(colour)
yield line
yield from reset()
yield "\n"
def enumerate_lines(stream: Iterator[str]) -> Iterator[Tuple[bool, int, str]]:
l_stream = unicode_width(stream)
prev: Optional[Tuple[int, str]] = next(l_stream, None)
x = 0 if prev is None else 1
def drain(ret: bool) -> Iterator[Tuple[bool, int, str]]:
nonlocal prev
if prev is not None:
yield (ret, *prev)
prev = None
for width, char in l_stream:
new = x + width
if new > COLS:
yield from drain(True)
prev = (width, char)
x = width
elif new == COLS or char == "\n":
yield from drain(False)
yield True, width, char
x = 0
else:
yield from drain(False)
prev = (width, char)
x = new
yield from drain(False)
def lerp(c1: RGB, c2: RGB, mix: float) -> RGB:
lhs = map(lambda c: c * mix, c1)
rhs = map(lambda c: c * (1 - mix), c2)
new = map(lambda c: int(round(sum(c))), zip(lhs, rhs))
return cast(RGB, tuple(new))
def sine_wave(t: float) -> float:
period = pi * pi
x = t * period
return (sin(x / pi + pi / 2) + 1) / 2
def interpolate_1d(palette: Palette, rep: int) -> Iterator[Iterator[RGB]]:
colours = cycle(palette)
def once() -> Iterator[RGB]:
prev = next(colours)
while True:
curr = next(colours)
for t in range(rep + 1):
mix = sine_wave(t / rep)
yield lerp(prev, curr, mix)
prev = curr
yield from repeat(once())
# contributed by https://github.com/nshepperd
# https://github.com/ms-jpq/gay/issues/2
def interpolate_2d(palette: Palette, rep: int) -> Iterator[Iterator[RGB]]:
num = len(palette)
for y in count():
def line() -> Iterator[RGB]:
for x in count():
p = (x + 1.5 * y) / rep
i = int(p)
prev = palette[i % num]
curr = palette[(i + 1) % num]
mix = sine_wave(p - i)
yield lerp(prev, curr, mix)
yield line()
def interpolation_for(
mode: Gradient, palette: Palette, rep: Optional[int]
) -> Iterator[Iterator[RGB]]:
if mode == Gradient.D1:
period = rep or randint(10, 20)
return interpolate_1d(palette, period)
if mode == Gradient.D2:
period = rep or randint(10, 15)
return interpolate_2d(palette, period)
else:
raise ValueError()
def colourize(
colour_space: ColourSpace, rgb_gen: Iterator[Iterator[RGB]], stream: Iterator[str],
) -> Iterator[str]:
fg, _, reset = decor_for(colour_space)
colour_gen = next(rgb_gen)
for new_line, width, char in enumerate_lines(stream):
if width:
colour = next(colour_gen)
yield from fg(colour)
yield char
if new_line:
yield from reset()
colour_gen = next(rgb_gen)
def main() -> None:
trap_sig()
args = parse_args()
register(on_exit)
colour_space = ColourSpace(args.colour)
palette = parse_raw_palette(FLAG_SPECS[args.flag])
if args.flag_only:
flag_stripes = paint_flag(colour_space=colour_space, palette=palette)
print(*flag_stripes, sep="", end="")
else:
stream = stdin_stream()
normalized_stream = normalize_width(args.tabs, stream)
interpolation = Gradient(args.interpolation)
rgb_gen = interpolation_for(
mode=interpolation, palette=palette, rep=args.period
)
gen = colourize(
colour_space=colour_space, rgb_gen=rgb_gen, stream=normalized_stream,
)
for chunk in iter(lambda: tuple(islice(gen, COLS)), ()):
print(*chunk, sep="", end="")
try:
main()
except KeyboardInterrupt:
exit(130)
except BrokenPipeError:
exit(13)