Since it's wrapped in a `<pre>`, I assume it's meant to be inserted into HTML?
362 lines
11 KiB
Python
Executable File
362 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# _ _ _ _ _ _
|
|
# | |_(_) |__| |___ | |_ _ _ __ _(_)_ _
|
|
# | _| | / _` / -_)| _| '_/ _` | | ' \
|
|
# \__|_|_\__,_\___(_)__|_| \__,_|_|_||_|
|
|
#
|
|
# tilde.train is an instance of TerminalTrain. It was originally developed
|
|
# by cmccabe on tilde.town (https://tildegit.org/cmccabe/TerminalTrain) but is now
|
|
# maintained by vilmibm.
|
|
#
|
|
# If you want to contribute code improvements, create a pull request here:
|
|
# https://git.tilde.town/vilmibm/tilde-train
|
|
#
|
|
# -----------------
|
|
#
|
|
# WHAT IS IT?
|
|
# like sl (the ls typo prank), but each car on the train is a x*y character ascii art
|
|
# produced by tilde.town.
|
|
#
|
|
# ( original sl source code of sl? https://github.com/mtoyoda/sl )
|
|
#
|
|
# TODO:
|
|
# * loosen the restriction on allowable characters in train cars. right now, limited
|
|
# to characters in python's string.printable list.
|
|
# * turn main loop into a function, so cmd line arg reader can call it (with -p) and quit.
|
|
# * figure out why tilde.train doesn't work in some terminals (sthg sthg unicode...)
|
|
# * BUGFIX-1 - something about inclusion default cars adding extra "links" to the train.
|
|
# * the -p (print train) option should print all cars, not limited to the max_cars value.
|
|
# * related to BUGFIX-1, that seems to impact spacers (links) between cars.
|
|
# * allow users to create multiple frames so their cars can be animated (difficulty=med+)
|
|
# * allow user configurable speed and number of train cars
|
|
# * allow users to move the train up or down with arrow keys
|
|
# -- worked with asciimatics, but python curses blocks on getch()
|
|
#
|
|
|
|
from random import shuffle # allowing us to randomize selection of cars.
|
|
import glob # allowing us to search the file system for .choochoo files.
|
|
import sys # so we can read command line arguments.
|
|
import curses
|
|
from signal import signal, SIGINT
|
|
import time # allowing the loop steps of train animation to be slowed
|
|
import string # for input validation
|
|
from html import escape
|
|
from inspect import cleandoc
|
|
from pathlib import Path
|
|
|
|
traincarFN = ".choochoo"
|
|
max_x = 35 # max length of train car.
|
|
max_y = 10 # max height of train car.
|
|
max_cars = 10 # most cars to include in one train.
|
|
print_train = False # print train to file (instead of the screen scroll)
|
|
|
|
train = [""]*max_y # empty train of correct height.
|
|
cars: list[list[str]] = []
|
|
|
|
engine = [
|
|
r" ____ ",
|
|
r" |____| ------------",
|
|
r" | | === | ------ |",
|
|
r" ___| |__| |_____| | O | |",
|
|
r" | | | |__/V\_| |",
|
|
r" [[ | |",
|
|
r" | | ------------ | ~town |",
|
|
r" |__|______________|__________|",
|
|
r" //// / _\__/__\__/__\ / \ ",
|
|
r"//// \__/ \__/ \__/ \__/ ",
|
|
]
|
|
|
|
caboose = [
|
|
r" || ",
|
|
r" ============= || ",
|
|
r"=========| |========== ",
|
|
r" | ---- ---- | ",
|
|
r" | | | | | | ",
|
|
r" | ---- ---- | ",
|
|
r" | tilde.town railways | ",
|
|
r"==| |== ",
|
|
r"== - / \-/ \-----/ \-/ \ - == ",
|
|
r" \__/ \__/ \__/ \__/ ",
|
|
]
|
|
|
|
default_car = [
|
|
r" ---------------------------- ",
|
|
r"| |",
|
|
r"| YOUR TRAIN CAR HERE! |",
|
|
r"| Just create a |",
|
|
r"| ~/.choochoo file! |",
|
|
r"| __ __ __ __ |",
|
|
r" - / \-/ \------/ \-/ \ - ",
|
|
r" \__/ \__/ \__/ \__/ ",
|
|
]
|
|
|
|
class CarError(Exception):
|
|
"""Error related validating a car."""
|
|
|
|
def print_help():
|
|
print(
|
|
cleandoc(
|
|
f"""
|
|
~ ~ Hooray! You've found the tilde.train! ~ ~
|
|
|
|
To add your own car to a future train, create
|
|
a .choochoo file in your home directory and
|
|
make sure it is 'other' readable, for example:
|
|
|
|
chmod 644 ~/.choochoo
|
|
|
|
The file should contain an ascii drawing of a
|
|
train car no more than {max_x} characters wide
|
|
and {max_y} characters tall.
|
|
|
|
Only printable ascii characters are accepted for now.
|
|
Run the command again followed by a -t switch to test
|
|
your .choochoo file and report any non accepted chars.
|
|
|
|
Each train contains a random selection of cars
|
|
from across tilde.town user home directories.
|
|
Don't worry, yours will be coming around the
|
|
bend soon!
|
|
|
|
~ ~ ~ ~ ~ ~
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_user_car():
|
|
fname = Path.home() / traincarFN
|
|
try:
|
|
choochoo_string = fname.read_text("utf-8")
|
|
except OSError as err:
|
|
raise OSError(
|
|
f"Couldn't open {fname}\n"
|
|
"Either it doesn't exist, or is not readble by the tilde.train script."
|
|
) from err
|
|
|
|
choochoo_list = choochoo_string.split("\n")
|
|
|
|
car = "\n".join(choochoo_list)
|
|
car2 = car.replace("\t", "") # do not allow tabs
|
|
car2 = car2.replace("\v", "") # do not allow vertical tabs
|
|
car2 = car2.replace("\f", "") # do not allow line feeds
|
|
car2 = car2.replace("\r", "") # do not allow carriage returns
|
|
car2 = ''.join([i if string.printable.find(i) >= 0 else ' ' for i in car2])
|
|
|
|
print("")
|
|
print("Test results:")
|
|
|
|
if car != car2:
|
|
print("")
|
|
print("Your train car contains an invalid character. Sorry, ")
|
|
print("for now only standard ascii characters are allowed.")
|
|
print("You can still use most other characters (except tabs),")
|
|
print("but they will be replaced by a standard ascii char when")
|
|
print("the train is built.")
|
|
|
|
bad_chars = []
|
|
for i in enumerate(car):
|
|
if string.printable.find(i[1]) < 0:
|
|
bad_chars.append(i[1])
|
|
if i[1] in ("\t", "\v", "\f", "\r"):
|
|
bad_chars.append("other whitespace")
|
|
bad_chars = set(bad_chars)
|
|
bad_chars = ", ".join(bad_chars)
|
|
|
|
print("")
|
|
print("The following currently the only accepted characters: ")
|
|
print(string.printable.strip())
|
|
print("")
|
|
print("Yours contained " + bad_chars)
|
|
sys.exit(1)
|
|
|
|
train_height = len(choochoo_list)
|
|
train_length = len(max(choochoo_list, key=len))
|
|
|
|
if train_height > max_y+1:
|
|
print("FAIL. Your train car is too tall.")
|
|
print("It should be no taller than " + str(max_y) + " lines in height.")
|
|
sys.exit(1)
|
|
|
|
if train_length > max_x:
|
|
print("FAIL. Your train car is too long.")
|
|
print("It should be no longer than " + str(max_x) + " characters in length.")
|
|
sys.exit(1)
|
|
|
|
print("PASS. Your train car will work on the tilde.town tracks! :)")
|
|
sys.exit()
|
|
|
|
def link_car(car: list[str]):
|
|
for idx,row in enumerate(car):
|
|
car[idx] = " " + row
|
|
car[len(car)-3] = "+" + car[len(car)-3][1:]
|
|
car[len(car)-2] = "+" + car[len(car)-2][1:]
|
|
return car
|
|
|
|
def validate_car(car: list[str]):
|
|
# this function (1) checks that a train car isn't too tall or too long
|
|
# (2) pads it vertically or on the right side if it is too short or if
|
|
# not all lines are the same length and (3) removes bad characters.
|
|
|
|
car_str = "\n".join(car)
|
|
car_str = ''.join([i if ord(i) < 128 else ' ' for i in car_str])
|
|
car_list = car_str.split("\n")
|
|
|
|
# remove blank lines from top and bottom of car,
|
|
# so we can estimate its true size.
|
|
while car_list[0].strip() == "":
|
|
car_list.pop(0) # clear top
|
|
while car_list[(len(car_list)-1)].strip() == "":
|
|
car_list.pop() # clear bottom
|
|
|
|
# len(car_list) is the height of the train car, in number of rows.
|
|
if len(car_list) > max_y+1:
|
|
raise CarError(f"Car is too tall ({len(car_list)} > {max_y + 1}).")
|
|
if len(car_list) == 0:
|
|
raise CarError(f"Car is empty.")
|
|
|
|
# vertically pad short cars with 1 space (lines will be lengthened later).
|
|
while len(car_list) < max_y:
|
|
car_list = [" "] + car_list
|
|
|
|
for idx,row in enumerate(car_list):
|
|
car_list[idx] = row.rstrip()
|
|
|
|
longest_line = len(max(car_list, key=len)) # longest line in .choochoo file.
|
|
|
|
for idx,row in enumerate(car_list):
|
|
if len(row) > max_x+1: # check length of each row in .choochoo file.
|
|
raise CarError(f"Car is too wide ({len(row)} > {max_x + 1}).")
|
|
elif "\t" in row or "\v" in row or "\f" in row or "\r" in row:
|
|
raise CarError(f"Car contains illegal control characters.")
|
|
elif len(row) < longest_line:
|
|
padding = " "*(longest_line - len(row))
|
|
car_list[idx] += padding # add padding spaces.
|
|
|
|
return car_list
|
|
|
|
|
|
def print_all_cars():
|
|
for fname in glob.glob('/home/*/' + traincarFN):
|
|
try:
|
|
with open(fname, 'r') as myfile:
|
|
choochoo_string = myfile.read()
|
|
choochoo_list = choochoo_string.split("\n")
|
|
if len(choochoo_list) > max_y+1:
|
|
continue # the train car was too tall; skip it.
|
|
|
|
car = validate_car(choochoo_list) # printing is only a DEBUG feature.
|
|
print("")
|
|
print(fname + ":")
|
|
print("\n".join(car)) # print the car to stdout
|
|
|
|
except:
|
|
pass
|
|
|
|
|
|
def chuggachugga(stdscr: curses.window):
|
|
curses.curs_set(0)
|
|
h, w = stdscr.getmaxyx()
|
|
x_pos = w-1
|
|
y_pos = int(round(h/2))
|
|
|
|
while True:
|
|
for idx,train_layer in enumerate(reversed(train)):
|
|
|
|
train_snip_start = 0 if (x_pos >= 0) else min(abs(x_pos), len(train_layer))
|
|
train_snip_end = w-x_pos if (w-x_pos <= len(train_layer)) else len(train_layer)
|
|
x = max(0, x_pos)
|
|
stdscr.addstr((y_pos-idx),x,train_layer[train_snip_start:train_snip_end])
|
|
|
|
x_pos -= 1
|
|
if x_pos == -train_len:
|
|
return
|
|
|
|
stdscr.refresh()
|
|
time.sleep(.03)
|
|
|
|
def handler(signal_received, frame):
|
|
print("Oops. The train broke. The engineer is looking into it!")
|
|
print("(Note: the train does not work in all terminals yet.)")
|
|
sys.exit(1)
|
|
|
|
default_car = validate_car(default_car)
|
|
|
|
if len(sys.argv) == 2 and ("-h" in sys.argv[1] or "help" in sys.argv[1]):
|
|
print_help()
|
|
sys.exit()
|
|
|
|
if len(sys.argv) == 2 and ("-t" in sys.argv[1] or "test" in sys.argv[1]):
|
|
test_user_car()
|
|
sys.exit()
|
|
|
|
if len(sys.argv) == 2 and ("-p" in sys.argv[1] or "print" in sys.argv[1]):
|
|
print_train = True
|
|
|
|
if len(sys.argv) == 2 and ("-a" in sys.argv[1] or "all" in sys.argv[1]):
|
|
print_all_cars()
|
|
sys.exit()
|
|
|
|
|
|
# start a loop that collects all .choochoo files and processes on in each loop
|
|
for fname in glob.glob('/home/*/' + traincarFN):
|
|
car_len = 1
|
|
try:
|
|
with open(fname, 'r') as myfile:
|
|
# print fname # debug, print file path and name
|
|
choochoo_string = myfile.read()
|
|
choochoo_list = choochoo_string.split("\n")
|
|
if len(choochoo_list) > max_y+1:
|
|
continue # the train car was too tall; skip it.
|
|
|
|
car = validate_car(choochoo_list) # printing is only a DEBUG feature.
|
|
cars.append(car) # start a list of lists (list of cars) here.
|
|
|
|
except:
|
|
pass
|
|
|
|
while len(cars) < max_cars:
|
|
cars.append([*default_car]) # add copies of default cars if train too short
|
|
|
|
shuffle(cars)
|
|
cars = cars[0:max_cars]
|
|
|
|
for idx,car in enumerate(cars):
|
|
cars[idx] = link_car(cars[idx])
|
|
|
|
cars.insert(0, engine)
|
|
caboose = link_car(caboose)
|
|
cars.append(caboose)
|
|
|
|
for i in cars:
|
|
n = 0
|
|
for j in i:
|
|
train[n] += j
|
|
n+=1
|
|
|
|
train_len = len(str(train[0]))
|
|
train_str = "\n".join(train)
|
|
|
|
if print_train:
|
|
print("<pre>")
|
|
print(escape(train_str))
|
|
print("</pre>")
|
|
sys.exit()
|
|
|
|
pad_str = " "*train_len
|
|
train.insert(0,pad_str)
|
|
train.append(pad_str)
|
|
|
|
if __name__ == "__main__":
|
|
signal(SIGINT, handler)
|
|
try:
|
|
curses.wrapper(chuggachugga)
|
|
except curses.error as err:
|
|
print(
|
|
f"{err}\n"
|
|
"Couldn't print the train for some reason. "
|
|
"Maybe your terminal window is too short?"
|
|
)
|
|
sys.exit(1)
|