1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
|
#!/usr/bin/env python3
import argparse
import enum
import fcntl
import random
import signal
import struct
import sys
import termios
from typing import Any
class ColorVariant(enum.IntEnum):
NONE = enum.auto()
REGULAR = enum.auto()
BRIGHT = enum.auto()
CUBE = enum.auto()
RGB = enum.auto()
def main() -> None:
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
lines: int | None = None
cols: int | None = None
width: int | None = None
height: int | None = None
if opts.rows is None or opts.cols is None:
try:
def dummy(*args: Any) -> None:
"""Need a handler installed for sigwait() to trigger."""
_ = args
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)))
assert width is not None
assert height is not None
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
assert lines is not None
height = 15 * lines # PGO helper binary hardcodes cell height to 15px
if opts.cols is not None:
cols = opts.cols
assert cols is not None
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: tuple[int, int] | None = None
last_size: tuple[int, int] = 0, 0
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 set to either 0 or 1 - opaque or transparent)
sixel_p2 = random.randrange(2)
out.write(f'\033P;{sixel_p2}q')
# 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__':
main()
|