Created
March 23, 2024 09:33
-
-
Save vmedea/4f0c177d324812113bb378d73ba0294b to your computer and use it in GitHub Desktop.
Print and colorize "dream stream" from dreams-of-an-electric-mind.webflow.io into the active terminal.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Mara Huldra 2024 | |
# SPDX-License-Identifier: MIT | |
''' | |
Print and colorize "dream stream" from dreams-of-an-electric-mind.webflow.io into the active terminal. | |
''' | |
import argparse | |
import datetime | |
from html.parser import HTMLParser | |
import os | |
import random | |
import sys | |
import textwrap | |
import time | |
from typing import Dict, List, Optional, Tuple | |
import urllib.request | |
URL = 'https://dreams-of-an-electric-mind.webflow.io/eternal' | |
BACKUP_DIR = 'data/' | |
# VGA DOS palette | |
VGA_PAL = [ | |
(0, 0, 0), # 0 black | |
(170, 0, 0), # 1 dark red | |
(0, 170, 0), # 2 dark green | |
(170, 85, 0), # 3 brown | |
(0, 0, 170), # 4 dark blue | |
(170, 0, 170), # 5 dark magenta | |
(0, 170, 170), # 6 dark cyan | |
(170, 170, 170), # 7 light grey | |
(85, 85, 85), # 8 dark grey | |
(255, 85, 85), # 9 light red | |
(85, 255, 85), # 10 light green | |
(255, 255, 85), # 11 yellow | |
(85, 85, 255), # 12 light blue | |
(255, 85, 255), # 13 light magenta | |
(85, 255, 255), # 14 light cyan | |
(255, 255, 255), # 15 white | |
] | |
MSG_COLOR = VGA_PAL[12] | |
MSG_H_COLOR = VGA_PAL[15] | |
NORMAL_COLOR = VGA_PAL[7] | |
DIGIT_COLOR = VGA_PAL[15] | |
PAREN_COLOR = VGA_PAL[8] | |
PUNCT_COLOR = VGA_PAL[6] | |
HIGH_COLOR = VGA_PAL[14] | |
CLEAR = '\x1b[2J' | |
RESET = '\x1b[0m' | |
out = sys.stdout | |
def term_attr(fg: Optional[Tuple[int, int, int]]=None, bg: Optional[Tuple[int, int, int]]=None) -> str: | |
seq = [] | |
if fg is not None: | |
seq.extend([38, 2, fg[0], fg[1], fg[2]]) | |
if bg is not None: | |
seq.extend([48, 2, bg[0], bg[1], bg[2]]) | |
return '\x1b[' + ';'.join((str(x) for x in seq)) + 'm' | |
def message(msg: str) -> None: | |
out.write(f'{term_attr(fg=MSG_H_COLOR)}* {term_attr(fg=MSG_COLOR)}{msg}{RESET}\n') | |
out.flush() | |
def download_dreams() -> str: | |
''' | |
Download dream from the site and return it, saving a copy. | |
''' | |
message('Downloading new dream') | |
req = urllib.request.Request(URL) | |
con = urllib.request.urlopen(req) | |
data = con.read() | |
data = data.decode('utf8') | |
os.makedirs(BACKUP_DIR, exist_ok=True) | |
backup_name = os.path.join(BACKUP_DIR, 'eternal.' + datetime.datetime.utcnow().strftime("%Y%d%m-%H%M%S")) | |
with open(backup_name, 'w') as f: | |
message(f'Saved a copy to {backup_name}') | |
f.write(data) | |
return data | |
class DreamHTMLParser(HTMLParser): | |
in_dream: bool = False | |
dreams: List[str] | |
def __init__(self) -> None: | |
super().__init__() | |
self.dreams = [] | |
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None: | |
if tag == 'pre' and dict(attrs).get('class', None) == 'dream-data': | |
self.in_dream = True | |
def handle_endtag(self, tag: str) -> None: | |
self.in_dream = False | |
def handle_data(self, data: str) -> None: | |
if self.in_dream: | |
self.dreams.append(data) | |
def load_dreams(data: str) -> List[str]: | |
''' | |
Parse the dreams from a html string in memory. | |
''' | |
parser = DreamHTMLParser() | |
parser.feed(data) | |
return parser.dreams | |
class Colorizer: | |
'''very basic category based character categorization for the terminal''' | |
# XXX could be stateful for more advanced highlighting/colorization. | |
def colorize(self, ch: str) -> str: | |
if ch in '[]()<>': | |
color = PAREN_COLOR | |
elif ch.isalpha(): | |
color = NORMAL_COLOR | |
elif ch.isdigit(): | |
color = DIGIT_COLOR | |
else: | |
if ord(ch) < 128: | |
color = PUNCT_COLOR | |
else: | |
color = HIGH_COLOR | |
return term_attr(fg=color) + ch | |
def newline(self) -> None: | |
pass | |
def print_dream(dream: str, width: int, skip_lines: int, char_delay: float) -> None: | |
c = Colorizer() | |
for para in dream.splitlines()[skip_lines:]: | |
para = textwrap.fill(para, width=width-1) | |
for ch in para: | |
out.write(c.colorize(ch)) | |
out.flush() | |
time.sleep(char_delay) | |
c.newline() | |
out.write(RESET + "\n") | |
out.flush() | |
def parse_args() -> argparse.Namespace: | |
parser = argparse.ArgumentParser( | |
prog='stream.py', | |
description='Show dreams-of-an-electric-mind.webflow.io dream stream', | |
epilog='By default, a new dream will be downloaded and randomly chosen.') | |
parser.add_argument('filename', metavar='FILE', type=str, nargs='?', | |
help='Input file name (html)') | |
parser.add_argument('num', metavar='N', type=int, nargs='?', | |
help='Dream sequence number within file') | |
parser.add_argument('-s', '--speed', type=float, default=50, | |
help='"Typing" speed (default 50)') | |
parser.add_argument('--no-clear', dest='clear', action='store_false', | |
help="Don't clear screen (default: clear)") | |
parser.add_argument('--clear', dest='clear', action='store_true', | |
help=argparse.SUPPRESS) | |
return parser.parse_args() | |
def main() -> None: | |
args = parse_args() | |
if args.clear: | |
out.write(CLEAR) | |
if args.filename is None: # Download from the internet. | |
data = download_dreams() | |
else: # Use a stored file. | |
with open(args.filename, 'r', encoding='utf8') as f: | |
data = f.read() | |
dreams = load_dreams(data) | |
termsize = os.get_terminal_size() | |
if args.num is not None: | |
dream = dreams[args.num] | |
else: | |
idx = random.randrange(0, len(dreams)) | |
message(f'Chose random sequence index {idx} (of {len(dreams)})') | |
dream = dreams[idx] | |
try: | |
print_dream(dream, termsize[0], 17, 1.0 / args.speed) | |
except KeyboardInterrupt: # in case of ctrl-C, print a newline first | |
out.write('\n') | |
finally: | |
out.write(RESET) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment