TerminalTrain is a shell script that build a train out of user contributed files from across the /home directory and runs the train across your terminal.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

371 lines
12 KiB

#!/usr/bin/env python3
## ___ _____ ___ _____ _
## | _ \_ _/ __|_ _| _ __ _(_)_ _
## | / | || (__ _| || '_/ _` | | ' \
## |_|_\ |_| \___(_)_||_| \__,_|_|_||_|
##
## TerminalTrain is called RTC.Train here because it is maintained
## at rawtext.club. You can, of course, rebrand it to whatever
## system you're running it on. Or, feel free to sign up for a free
## rawtext.club account and help us build this and other tools!
##
## created originally by cmccabe@tilde.town - spring 2018-ish.
## ported from asciimatics to python curses late 2019.
##
## WHAT?
## like sl (the ls typo prank), but each car on the train is a x*y character ascii art
## produced by rawtext.club members.
##
## ( 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 rtc.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
import curses
from signal import signal, SIGINT
import time ## allowing the loop steps of train animation to be slowed
import string ## for input validation
traincarFN = ".choochoo"
max_x = 35 ## max length of train car.
max_y = 10 ## max height of train car.
max_cars = 2 ## 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 = []
engine = """ ____
|____| ------------
| | === | ------ |
___| |__| |_____| | O | |
| | | |__/V\_| |
[[ | |
| | ------------ | rtc |
|__|______________|__________|
//// / _\__/__\__/__\ / \
//// \__/ \__/ \__/ \__/ """
engine = engine.split("\n")
caboose = """ ||
============= ||
=========| |==========
| ---- ---- |
| | | | | |
| ---- ---- |
| rtc railways |
==| |==
== - / \-/ \-----/ \-/ \ - ==
\__/ \__/ \__/ \__/ """
caboose = caboose.split("\n")
default_car = """ ----------------------------
| |
| YOUR TRAIN CAR HERE! |
| Just create a |
| ~/.choochoo file! |
| __ __ __ __ |
- / \-/ \------/ \-/ \ -
\__/ \__/ \__/ \__/"""
default_car = default_car.split("\n")
def print_help():
print("")
print("~ ~ Hooray! You've found the rtc.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 rawtext.club user home directories.")
print("Don't worry, yours will be coming around the")
print("bend soon!")
print("")
print("~ ~ ~ ~ ~ ~")
print("")
def test_user_car():
username = getpass.getuser()
fname = "/home/" + username + "/" + 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 rtc.train script.")
exit()
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 = ''.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)
exit()
# print("")
# print("Test results:")
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.")
myfile.close()
exit()
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()
print("PASS. Your train car will work on the rtc.tracks! :)")
myfile.close()
exit()
def link_car(car):
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.
car = "\n".join(car)
car = ''.join([i if ord(i) < 128 else ' ' for i in car])
car = car.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
## 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.
## vertically pad short cars with 1 space (lines will be lengthened later).
while len(car) < max_y:
car = [" "] + car
for idx,row in enumerate(car):
car[idx] = row.rstrip()
longest_line = len(max(car, 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.
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
elif len(row) < longest_line:
padding = " "*(longest_line - len(row))
car[idx] += padding ## add padding spaces.
return car
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), ...?
except:
pass;
# print "Cannot open " + fname # for debuggering purposes
def chuggachugga(stdscr):
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)):
# 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)
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()
if len(sys.argv) == 2 and ("-t" in sys.argv[1] or "test" in sys.argv[1]):
test_user_car()
quit()
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()
## 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.
if car != 0:
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
while len(cars) < max_cars:
cars.append(default_car) ## add 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(train_str)
print("</pre>")
quit()
pad_str = " "*train_len
train.insert(0,pad_str)
train.append(pad_str)
if __name__ == "__main__":
signal(SIGINT, handler)
curses.wrapper(chuggachugga)