Skip to content

Instantly share code, notes, and snippets.

@vmedea
Created March 23, 2024 09:33
Show Gist options
  • Save vmedea/4f0c177d324812113bb378d73ba0294b to your computer and use it in GitHub Desktop.
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.
#!/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