mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
P2=1 means "empty sixels remain at their current color". This is usually the case with modern sixel encoders.
256 lines
9.1 KiB
Python
Executable file
256 lines
9.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
import argparse
|
||
import enum
|
||
import fcntl
|
||
import random
|
||
import signal
|
||
import struct
|
||
import sys
|
||
import termios
|
||
|
||
|
||
class ColorVariant(enum.IntEnum):
|
||
NONE = enum.auto()
|
||
REGULAR = enum.auto()
|
||
BRIGHT = enum.auto()
|
||
CUBE = enum.auto()
|
||
RGB = enum.auto()
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument(
|
||
'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file')
|
||
parser.add_argument('--cols', type=int)
|
||
parser.add_argument('--rows', type=int)
|
||
parser.add_argument('--colors-regular', action='store_true')
|
||
parser.add_argument('--colors-bright', action='store_true')
|
||
parser.add_argument('--colors-256', action='store_true')
|
||
parser.add_argument('--colors-rgb', action='store_true')
|
||
parser.add_argument('--scroll', action='store_true')
|
||
parser.add_argument('--scroll-region', action='store_true')
|
||
parser.add_argument('--attr-bold', action='store_true')
|
||
parser.add_argument('--attr-italic', action='store_true')
|
||
parser.add_argument('--attr-underline', action='store_true')
|
||
parser.add_argument('--sixel', action='store_true')
|
||
parser.add_argument('--seed', type=int)
|
||
|
||
opts = parser.parse_args()
|
||
out = opts.out if opts.out is not None else sys.stdout
|
||
|
||
if opts.rows is None or opts.cols is None:
|
||
try:
|
||
def dummy(*args):
|
||
"""Need a handler installed for sigwait() to trigger."""
|
||
pass
|
||
signal.signal(signal.SIGWINCH, dummy)
|
||
|
||
while True:
|
||
with open('/dev/tty', 'rb') as pty:
|
||
lines, cols, height, width = struct.unpack(
|
||
'HHHH',
|
||
fcntl.ioctl(pty,
|
||
termios.TIOCGWINSZ,
|
||
struct.pack('HHHH', 0, 0, 0, 0)))
|
||
|
||
if width > 0 and height > 0:
|
||
break
|
||
|
||
# We’re early; the foot window hasn’t been mapped yet. Or,
|
||
# to be more precise, fonts haven’t yet been loaded,
|
||
# meaning it doesn’t have any cell geometry yet.
|
||
signal.sigwait([signal.SIGWINCH])
|
||
|
||
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||
|
||
except OSError:
|
||
lines = None
|
||
cols = None
|
||
height = None
|
||
width = None
|
||
|
||
if opts.rows is not None:
|
||
lines = opts.rows
|
||
height = 15 * lines # PGO helper binary hardcodes cell height to 15px
|
||
if opts.cols is not None:
|
||
cols = opts.cols
|
||
width = 8 * cols # PGO help binary hardcodes cell width to 8px
|
||
|
||
if lines is None or cols is None or height is None or width is None:
|
||
raise Exception('could not get terminal width/height; use --rows and --cols')
|
||
|
||
assert lines > 0, f'{lines}'
|
||
assert cols > 0, f'{cols}'
|
||
assert width > 0, f'{width}'
|
||
assert height > 0, f'{height}'
|
||
|
||
# Number of characters to write to screen
|
||
count = 256 * 1024**1
|
||
|
||
# Characters to choose from
|
||
alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789 öäå 👨👩🧒👩🏽🔬🇸🇪'
|
||
|
||
color_variants = ([ColorVariant.NONE] +
|
||
([ColorVariant.REGULAR] if opts.colors_regular else []) +
|
||
([ColorVariant.BRIGHT] if opts.colors_bright else []) +
|
||
([ColorVariant.CUBE] if opts.colors_256 else []) +
|
||
([ColorVariant.RGB] if opts.colors_rgb else []))
|
||
|
||
# Enter alt screen
|
||
out.write('\033[?1049h')
|
||
|
||
# uses system time or /dev/urandom if available if opt.seed == None
|
||
# pin seeding method to make seeding stable across future versions
|
||
random.seed(a=opts.seed, version=2)
|
||
|
||
for _ in range(count):
|
||
if opts.scroll and random.randrange(256) == 0:
|
||
out.write('\033[m')
|
||
|
||
if opts.scroll_region and random.randrange(256) == 0:
|
||
top = random.randrange(3)
|
||
bottom = random.randrange(3)
|
||
out.write(f'\033[{top};{lines - bottom}r')
|
||
|
||
lines_to_scroll = random.randrange(lines - 1)
|
||
rev = random.randrange(2)
|
||
if not rev and random.randrange(2):
|
||
out.write(f'\033[{lines};{cols}H')
|
||
out.write('\n' * lines_to_scroll)
|
||
else:
|
||
out.write(f'\033[{lines_to_scroll + 1}{"T" if rev == 1 else "S"}')
|
||
continue
|
||
|
||
# Generate a random location and a random character
|
||
row = random.randrange(lines)
|
||
col = random.randrange(cols)
|
||
c = random.choice(alphabet)
|
||
|
||
repeat = random.randrange((cols - col) + 1)
|
||
assert col + repeat <= cols
|
||
|
||
color_variant = random.choice(color_variants)
|
||
|
||
# Position cursor
|
||
out.write(f'\033[{row + 1};{col + 1}H')
|
||
|
||
if color_variant in [ColorVariant.REGULAR, ColorVariant.BRIGHT]:
|
||
do_bg = random.randrange(2)
|
||
base = 40 if do_bg else 30
|
||
base += 60 if color_variant == ColorVariant.BRIGHT else 0
|
||
|
||
idx = random.randrange(8)
|
||
out.write(f'\033[{base + idx}m')
|
||
|
||
elif color_variant == ColorVariant.CUBE:
|
||
do_bg = random.randrange(2)
|
||
base = 48 if do_bg else 38
|
||
|
||
idx = random.randrange(256)
|
||
if random.randrange(2):
|
||
# Old-style
|
||
out.write(f'\033[{base};5;{idx}m')
|
||
else:
|
||
# New-style (sub-parameter based)
|
||
out.write(f'\033[{base}:5:{idx}m')
|
||
|
||
elif color_variant == ColorVariant.RGB:
|
||
do_bg = random.randrange(2)
|
||
base = 48 if do_bg else 38
|
||
|
||
# use list comprehension in favor of randbytes(n)
|
||
# which is only available for Python >= 3.9
|
||
rgb = [random.randrange(256) for _ in range(3)]
|
||
|
||
if random.randrange(2):
|
||
# Old-style
|
||
out.write(f'\033[{base};2;{rgb[0]};{rgb[1]};{rgb[2]}m')
|
||
else:
|
||
# New-style (sub-parameter based)
|
||
out.write(f'\033[{base}:2::{rgb[0]}:{rgb[1]}:{rgb[2]}m')
|
||
|
||
if opts.attr_bold and random.randrange(5) == 0:
|
||
out.write('\033[1m')
|
||
if opts.attr_italic and random.randrange(5) == 0:
|
||
out.write('\033[3m')
|
||
if opts.attr_underline and random.randrange(5) == 0:
|
||
out.write('\033[4m')
|
||
|
||
out.write(c * repeat)
|
||
|
||
do_sgr_reset = random.randrange(2)
|
||
if do_sgr_reset:
|
||
reset_actions = ['\033[m', '\033[39m', '\033[49m']
|
||
out.write(random.choice(reset_actions))
|
||
|
||
# Reset colors
|
||
out.write('\033[m\033[r')
|
||
|
||
if opts.sixel:
|
||
# The sixel 'alphabet'
|
||
sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
|
||
|
||
last_pos = None
|
||
last_size = None
|
||
|
||
for _ in range(20):
|
||
if last_pos is not None and random.randrange(2):
|
||
# Overwrite last sixel. I.e. use same position and
|
||
# size as last sixel
|
||
pass
|
||
else:
|
||
# Random origin in upper left quadrant
|
||
last_pos = random.randrange(lines // 2) + 1, random.randrange(cols // 2) + 1
|
||
last_size = random.randrange((height + 1) // 2), random.randrange((width + 1) // 2)
|
||
|
||
out.write(f'\033[{last_pos[0]};{last_pos[1]}H')
|
||
six_height, six_width = last_size
|
||
six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels
|
||
|
||
# Begin sixel (with P2=1 - empty sixels are transparent)
|
||
out.write('\033P;1q')
|
||
|
||
# Sixel size. Without this, sixels will be
|
||
# auto-resized on cell-boundaries.
|
||
out.write(f'"1;1;{six_width};{six_height}')
|
||
|
||
# Set up 256 random colors
|
||
for idx in range(256):
|
||
# param 2: 1=HLS, 2=RGB.
|
||
# param 3/4/5: HLS/RGB values in range 0-100
|
||
# (except 'hue' which is 0..360)
|
||
out.write(f'#{idx};2;{random.randrange(101)};{random.randrange(101)};{random.randrange(101)}')
|
||
|
||
for row in range(six_rows):
|
||
band_count = random.randrange(4, 33)
|
||
for band in range(band_count):
|
||
# Choose a random color
|
||
out.write(f'#{random.randrange(256)}')
|
||
|
||
if random.randrange(2):
|
||
for col in range(six_width):
|
||
out.write(f'{random.choice(sixels)}')
|
||
else:
|
||
pix_left = six_width
|
||
while pix_left > 0:
|
||
repeat_count = random.randrange(1, pix_left + 1)
|
||
out.write(f'!{repeat_count}{random.choice(sixels)}')
|
||
pix_left -= repeat_count
|
||
|
||
# Next line
|
||
if band + 1 < band_count:
|
||
# Move cursor to beginning of current row
|
||
out.write('$')
|
||
elif row + 1 < six_rows:
|
||
# Newline
|
||
out.write('-')
|
||
|
||
# End sixel
|
||
out.write('\033\\')
|
||
|
||
# Leave alt screen
|
||
out.write('\033[?1049l')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|