diff --git a/tilde-train.py b/tilde-train.py index 21d8fa6..2251870 100755 --- a/tilde-train.py +++ b/tilde-train.py @@ -1,138 +1,147 @@ #!/usr/bin/env python3 -## _ _ _ _ _ _ -## | |_(_) |__| |___ | |_ _ _ __ _(_)_ _ -## | _| | / _` / -_)| _| '_/ _` | | ' \ -## \__|_|_\__,_\___(_)__|_| \__,_|_|_||_| -## -## tilde.train is an instance of TerminalTrain. It was originally developed -## by cmccabe on tilde.town but is now maintained in tildegit with the main -## development ocurring on rawtext.club. -## -## If you want to contribute to code improvement, or if you have suggestions -## for cmccabe, create a pull request here: https://tildegit.org/cmccabe/TerminalTrain -## or email cmccabe at cmccabe@rawtext.club -## -## ----------------- -## -## 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() -## +# _ _ _ _ _ _ +# | |_(_) |__| |___ | |_ _ _ __ _(_)_ _ +# | _| | / _` / -_)| _| '_/ _` | | ' \ +# \__|_|_\__,_\___(_)__|_| \__,_|_|_||_| +# +# 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 getpass ## so we can get the user's username +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 +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) +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 = [] +train = [""]*max_y # empty train of correct height. +cars: list[list[str]] = [] -engine = """ ____ - |____| ------------ - | | === | ------ | - ___| |__| |_____| | O | | - | | | |__/V\_| | - [[ | | - | | ------------ | ~town | - |__|______________|__________| - //// / _\__/__\__/__\ / \ -//// \__/ \__/ \__/ \__/ """ -engine = engine.split("\n") +engine = [ + r" ____ ", + r" |____| ------------", + r" | | === | ------ |", + r" ___| |__| |_____| | O | |", + r" | | | |__/V\_| |", + r" [[ | |", + r" | | ------------ | ~town |", + r" |__|______________|__________|", + r" //// / _\__/__\__/__\ / \ ", + r"//// \__/ \__/ \__/ \__/ ", +] -caboose = """ || - ============= || -=========| |========== - | ---- ---- | - | | | | | | - | ---- ---- | - | tilde.town railways | -==| |== -== - / \-/ \-----/ \-/ \ - == - \__/ \__/ \__/ \__/ """ -caboose = caboose.split("\n") +caboose = [ + r" || ", + r" ============= || ", + r"=========| |========== ", + r" | ---- ---- | ", + r" | | | | | | ", + r" | ---- ---- | ", + r" | tilde.town railways | ", + r"==| |== ", + r"== - / \-/ \-----/ \-/ \ - == ", + r" \__/ \__/ \__/ \__/ ", +] -default_car = """ ---------------------------- -| | -| YOUR TRAIN CAR HERE! | -| Just create a | -| ~/.choochoo file! | -| __ __ __ __ | - - / \-/ \------/ \-/ \ - - \__/ \__/ \__/ \__/""" -default_car = default_car.split("\n") +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("") - print("~ ~ Hooray! You've found the tilde.train! ~ ~") - print("") - print("To add your own car to a future train, create") - print("a .choochoo file in your home directory and") - print("make sure it is 'other' readable, for example:") - print("") - print(" chmod 644 ~/.choochoo") - print("") - print("The file should contain an ascii drawing of a") - print("train car no more than " + str(max_x) + " characters wide") - print("and " + str(max_y) + " characters tall.") - print("") - print("Only printable ascii characters are accepted for now.") - print("Run the command again followed by a -t switch to test") - print("your .choochoo file and report any non accepted chars.") - print("") - print("Each train contains a random selection of cars") - print("from across tilde.town user home directories.") - print("Don't worry, yours will be coming around the") - print("bend soon!") - print("") - print("~ ~ ~ ~ ~ ~") - print("") + 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(): - username = getpass.getuser() - fname = "/home/" + username + "/" + traincarFN + fname = Path.home() / traincarFN try: - myfile = open(fname, 'r') - except: - print("ERROR: Couldn't open " + fname) - print("Either it doesn't exist, or is not readble by the tilde.train script.") - exit() + 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_string = myfile.read() 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 = 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("") @@ -160,10 +169,7 @@ def test_user_car(): print(string.printable.strip()) print("") print("Yours contained " + bad_chars) - exit() - -# print("") -# print("Test results:") + sys.exit(1) train_height = len(choochoo_list) train_length = len(max(choochoo_list, key=len)) @@ -171,90 +177,85 @@ def test_user_car(): 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.") - myfile.close() - exit() + 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.") - myfile.close() - exit() + sys.exit(1) print("PASS. Your train car will work on the tilde.town tracks! :)") - myfile.close() - exit() + sys.exit() -def link_car(car): +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): -## 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. +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 = "\n".join(car) - car = ''.join([i if ord(i) < 128 else ' ' for i in car]) - car = car.split("\n") + 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[0].strip() == "": - car.pop(0) ## clear top - while car[(len(car)-1)].strip() == "": - car.pop() ## clear bottom + # 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(choochoo_list) is the height of the train car, in number of rows. - if len(car) > max_y+1 or len(car) == 0: - return 0 ## train car too tall or non-existant; skip it. + # 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) < max_y: - car = [" "] + car + # 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): - car[idx] = row.rstrip() + for idx,row in enumerate(car_list): + car_list[idx] = row.rstrip() - longest_line = len(max(car, key=len)) ## longest line in .choochoo file. + longest_line = len(max(car_list, key=len)) # longest line in .choochoo file. - for idx,row in enumerate(car): - if len(row) > max_x+1: ## check length of each row in .choochoo file. - return 0 ## train car too long; skip it. + 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: - return 0 # skip if contains tabs, vert tabs, line feeds or carriage ret + raise CarError(f"Car contains illegal control characters.") elif len(row) < longest_line: padding = " "*(longest_line - len(row)) - car[idx] += padding ## add padding spaces. + car_list[idx] += padding # add padding spaces. - return car + return car_list def print_all_cars(): for fname in glob.glob('/home/*/' + traincarFN): 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. - if car != 0: - print("") - print(fname + ":") - print("\n".join(car)) ## print the car to stdout - - ## HOW TO CLOSE THE FILE HANDLE? fname.close(), close(fname), ...? + 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; -# print "Cannot open " + fname # for debuggering purposes + pass -def chuggachugga(stdscr): +def chuggachugga(stdscr: curses.window): curses.curs_set(0) h, w = stdscr.getmaxyx() x_pos = w-1 @@ -262,82 +263,61 @@ def chuggachugga(stdscr): while True: for idx,train_layer in enumerate(reversed(train)): -# screen.print_at(train_layer, x_pos, (y_pos-idx)) -# stdscr.addstr((y_pos-idx),x_pos,train_layer) 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) -# train_snip_end = min(len(train_layer),w-x_pos) x = max(0, x_pos) -# stdscr.addstr(2,2,"start: " + str(train_snip_start)) -# stdscr.addstr(3,2,"end: "+ str(train_snip_end)) -# stdscr.addstr(4,2,"x_pos: " + str(x_pos)) -# stdscr.addstr(5,2,"train len: " + str(len(train_layer))) stdscr.addstr((y_pos-idx),x,train_layer[train_snip_start:train_snip_end]) x_pos -= 1 if x_pos == -train_len: -# stdscr.addstr(2,5,"here we are!") -# time.sleep(10) return stdscr.refresh() time.sleep(.03) -# ev = stdscr.getch() ## seems like curses waits for input, which won't work here. -# if ev in (ord('Q'), ord('q')): -# return -# elif ev == curses.KEY_UP: ## up -# y_pos += 1 -# elif ev == curses.KEY_DOWN: ## down -# y_pos -= 1 - - 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.)") - exit(0) + 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() - quit() + sys.exit() if len(sys.argv) == 2 and ("-t" in sys.argv[1] or "test" in sys.argv[1]): test_user_car() - quit() + 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() - quit() + sys.exit() -## start a loop that collects all .choochoo files and processes on in each loop +# 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 + # 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. + continue # the train car was too tall; skip it. - car = validate_car(choochoo_list) ## printing is only a DEBUG feature. - if car != 0: - cars.append(car) ## start a list of lists (list of cars) here. + car = validate_car(choochoo_list) # printing is only a DEBUG feature. + cars.append(car) # start a list of lists (list of cars) here. - ## HOW TO CLOSE THE FILE HANDLE? fname.close(), close(fname), ...? except: - pass; - ##print "Cannot open " + fname # for debuggering purposes + pass while len(cars) < max_cars: - cars.append(default_car) ## add default cars if train too short + cars.append([*default_car]) # add copies of default cars if train too short shuffle(cars) cars = cars[0:max_cars] @@ -360,14 +340,22 @@ train_str = "\n".join(train) if print_train: print("
")
-  print(train_str)
+  print(escape(train_str))
   print("
") - quit() + sys.exit() pad_str = " "*train_len train.insert(0,pad_str) train.append(pad_str) if __name__ == "__main__": - signal(SIGINT, handler) - curses.wrapper(chuggachugga) + 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)